import sys
import os

# 解决Windows打包exe环境下的编码问题
if sys.platform == 'win32':
    # 设置标准输出为UTF-8编码
    import io
    if hasattr(sys.stdout, 'buffer'):
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
    if hasattr(sys.stderr, 'buffer'):
        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
    # 设置环境变量
    os.environ['PYTHONIOENCODING'] = 'utf-8'

# 解析命令行参数
import argparse
parser = argparse.ArgumentParser(description='角标识别程序')
parser.add_argument('--input-dir', type=str, help='输入目录（监控PDF/PNG文件的目录）')
parser.add_argument('--output-dir', type=str, help='输出目录（保存识别结果的目录）')
parser.add_argument('--ws-host', type=str, default='0.0.0.0', help='WebSocket服务监听地址')
parser.add_argument('--ws-port', type=int, default=8766, help='WebSocket服务端口')
args = parser.parse_args()

# 保存命令行参数到全局变量
CMD_INPUT_DIR = args.input_dir
CMD_OUTPUT_DIR = args.output_dir

import fitz  # PyMuPDF
from PIL import Image
import numpy as np
import os
import json
import cv2
import numpy as np
import os
import time
import base64
from openai import OpenAI
import pandas as pd
from openpyxl import Workbook
from openpyxl.drawing.image import Image as ExcelImage
from openpyxl.styles import Alignment, PatternFill
from paddleocr import PaddleOCR, draw_ocr
import logging
import os
import numpy as np
import cv2
from PIL import Image

import os
import sys
import pandas as pd
import glob
import re
from openpyxl import load_workbook, Workbook
from openpyxl.utils import get_column_letter
from openpyxl.styles import Alignment

import qrcode
from PIL import Image
import io
from openpyxl.drawing.image import Image as XLImage
import tempfile
import openpyxl

import psutil # pip install psutil -i https://pypi.tuna.tsinghua.edu.cn/simple
import datetime
import socket
import asyncio
import threading
import websockets

from PIL import Image, ImageDraw, ImageFont
import svgwrite
from svgwrite import cm, mm
from typing import Optional
from urllib.parse import quote, urlparse, unquote
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
import posixpath

# pip install qrcode[pil] svgwrite -i https://pypi.tuna.tsinghua.edu.cn/simple

# .\.venv\Scripts\activate.bat
# pyarmor obfuscate main.py # 
# pyinstaller --onefile --name 角标识别_v13 角标识别.py --collect-all paddleocr

调试模式 = 0 # 调试的时候在 调试终端 打开

# 如果脚本所在目录是 dist，则删除和当前 脚本同级的 1.py、2.py、3.py
if os.path.basename(os.getcwd()) == "dist":
    for file in os.listdir(os.getcwd()):
        if file.endswith("1.py") or file.endswith("2.py") or file.endswith("test.py"):
            os.remove(file)

# 原1.py内容
def get_base_dir():
    """获取基础目录（兼容打包后的exe和直接运行）"""
    if getattr(sys, 'frozen', False):
        # 打包后的exe所在目录
        return os.path.dirname(sys.executable)
    else:
        # 直接运行时的脚本所在目录
        return os.path.dirname(os.path.abspath(__file__))
    
# 新增：数据库查询功能
import sqlite3

ORDER_DB_PATH = os.path.join(get_base_dir(), '../orders.db') if getattr(sys, 'frozen', False) else os.path.join(get_base_dir(), './orders.db')
    
DB_LOCK = threading.Lock()

WEBSOCKET_HOST = args.ws_host
WEBSOCKET_PORT = args.ws_port

connected_clients = set()
websocket_loop: Optional[asyncio.AbstractEventLoop] = None

ORDER_STATUS_FIELDS = ["serial_number", "identifier", "status", "pdf_name", "black_box_path", "updated_at"]

ORDER_STATUS_TABLE = """
CREATE TABLE IF NOT EXISTS order_status (
    serial_number TEXT NOT NULL,
    identifier TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT '未制作',
    pdf_name TEXT DEFAULT '',
    black_box_path TEXT DEFAULT '',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY(serial_number)
);
"""

ORDER_STATUS_ADD_COLUMNS = [
    ("black_box_path", "TEXT", ""),
]

FILE_SERVER_PORT = 8767
FILE_SERVER_BASE_URL = ""
FILE_SERVER_ROOT = ""
_file_server_httpd: Optional[ThreadingHTTPServer] = None
_file_server_thread: Optional[threading.Thread] = None

class FilesRequestHandler(SimpleHTTPRequestHandler):
    def translate_path(self, path: str) -> str:
        if not FILE_SERVER_ROOT:
            return super().translate_path(path)

        parsed = urlparse(path)
        request_path = parsed.path or ""
        if request_path.startswith("/files/"):
            rel_path = request_path[len("/files/") :]
        elif request_path.startswith("/files"):
            rel_path = request_path[len("/files") :]
        else:
            return super().translate_path(path)

        rel_path = posixpath.normpath(unquote(rel_path))
        rel_path = rel_path.lstrip("/\\")
        return os.path.join(FILE_SERVER_ROOT, rel_path)

    def do_GET(self):
        if not FILE_SERVER_ROOT:
            self.send_error(503, "File server root not configured")
            return
        if self.path.startswith("/files"):
            return super().do_GET()
        self.send_error(404, "Not Found")

    def do_HEAD(self):
        if not FILE_SERVER_ROOT:
            self.send_error(503, "File server root not configured")
            return
        if self.path.startswith("/files"):
            return super().do_HEAD()
        self.send_error(404, "Not Found")

    def log_message(self, format: str, *args):
        # 避免终端大量日志输出
        return

def start_file_server(root_dir: Optional[str]):
    global FILE_SERVER_ROOT, _file_server_httpd, _file_server_thread, FILE_SERVER_BASE_URL
    if not root_dir:
        return

    root_dir = os.path.abspath(root_dir)
    if not os.path.exists(root_dir):
        try:
            os.makedirs(root_dir, exist_ok=True)
        except Exception as exc:
            print(f"⚠️ 创建文件服务根目录失败: {exc}")
            return

    root_changed = FILE_SERVER_ROOT != root_dir
    FILE_SERVER_ROOT = root_dir
    FILE_SERVER_BASE_URL = f"http://{get_local_ip()}:{FILE_SERVER_PORT}"

    if _file_server_httpd:
        if root_changed:
            print(f"ℹ️ 文件服务根目录已更新为: {FILE_SERVER_ROOT}")
        return

    try:
        _file_server_httpd = ThreadingHTTPServer(("0.0.0.0", FILE_SERVER_PORT), FilesRequestHandler)
        _file_server_thread = threading.Thread(target=_file_server_httpd.serve_forever, daemon=True)
        _file_server_thread.start()
        print(f"✅ 文件服务已启动: {FILE_SERVER_BASE_URL}/files (根目录: {FILE_SERVER_ROOT})")
    except OSError as exc:
        print(f"❌ 启动文件服务失败: {exc}")
        _file_server_httpd = None
        _file_server_thread = None

def init_databases():
    """初始化SQLite数据库（包含订单状态表）"""
    try:
        with DB_LOCK:
            conn = sqlite3.connect(ORDER_DB_PATH)
            cursor = conn.cursor()
            cursor.execute("PRAGMA journal_mode=WAL;")
            cursor.execute(ORDER_STATUS_TABLE)
            for column_name, column_type, default in ORDER_STATUS_ADD_COLUMNS:
                cursor.execute(
                    "SELECT name FROM pragma_table_info('order_status') WHERE name = ?",
                    (column_name,),
                )
                if cursor.fetchone() is None:
                    default_clause = f" DEFAULT '{default}'" if default != "" else ""
                    cursor.execute(
                        f"ALTER TABLE order_status ADD COLUMN {column_name} {column_type}{default_clause}"
                    )
            conn.commit()
            conn.close()
        print("✅ 订单状态表初始化完成")
    except Exception as e:
        print(f"❌ 初始化订单状态表失败: {e}")

def get_db_connection():
    """获取orders.db连接"""
    conn = sqlite3.connect(ORDER_DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

def upsert_order_status(serial_number: str,
                        identifier: Optional[str] = None,
                        status: Optional[str] = None,
                        pdf_name: Optional[str] = None,
                        black_box_path: Optional[str] = None,
                        notify: bool = True):
    """新增或更新订单状态记录"""
    if not serial_number:
        return
    with DB_LOCK:
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute(
            "SELECT serial_number FROM order_status WHERE serial_number = ? LIMIT 1",
            (serial_number,)
        )
        exists = cursor.fetchone()
        fields = []
        values = []
        if identifier is not None:
            fields.append("identifier = ?")
            values.append(identifier)
        if status is not None:
            fields.append("status = ?")
            values.append(status)
        if pdf_name is not None:
            fields.append("pdf_name = ?")
            values.append(pdf_name)
        if black_box_path is not None:
            fields.append("black_box_path = ?")
            values.append(black_box_path)
        values.append(serial_number)
        if exists:
            if fields:
                fields.append("updated_at = CURRENT_TIMESTAMP")
                cursor.execute(
                    f"UPDATE order_status SET {', '.join(fields)} WHERE serial_number = ?",
                    tuple(values)
                )
        else:
            cursor.execute(
                "INSERT INTO order_status (serial_number, identifier, status, pdf_name, black_box_path) VALUES (?, ?, ?, ?, ?)",
                (
                    serial_number,
                    identifier or "",
                    status or "未制作",
                    pdf_name or "",
                    black_box_path or "",
                )
            )
        conn.commit()
        cursor.execute(
            "SELECT serial_number, identifier, status, pdf_name, black_box_path, updated_at FROM order_status "
            "WHERE serial_number = ?",
            (serial_number,)
        )
        record = cursor.fetchone()
        conn.close()
    if record and notify:
        broadcast_order_status(dict(record))

def get_all_order_status():
    with DB_LOCK:
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute(
            "SELECT serial_number, identifier, status, pdf_name, black_box_path, updated_at FROM order_status"
        )
        rows = [dict(row) for row in cursor.fetchall()]
        conn.close()
    return rows

def get_order_status(serial_number: str):
    with DB_LOCK:
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute(
            "SELECT serial_number, identifier, status, pdf_name, black_box_path, updated_at FROM order_status "
            "WHERE serial_number = ?",
            (serial_number,)
        )
        row = cursor.fetchone()
        conn.close()
    return dict(row) if row else None

async def handle_websocket(websocket):
    connected_clients.add(websocket)
    try:
        snapshot = [build_record_payload(item) for item in get_all_order_status()]
        await websocket.send(json.dumps({
            "type": "order_status_snapshot",
            "data": snapshot
        }, ensure_ascii=False))
        async for message in websocket:
            try:
                request = json.loads(message)
            except json.JSONDecodeError:
                await websocket.send(json.dumps({"type": "error", "message": "JSON解析失败"}))
                continue
            msg_type = request.get("type")
            if msg_type == "get_status":
                serial_number = request.get("serial_number")
                if serial_number:
                    data = get_order_status(serial_number)
                    data = build_record_payload(data) if data else None
                else:
                    data = [build_record_payload(item) for item in get_all_order_status()]
                await websocket.send(json.dumps({
                    "type": "order_status",
                    "data": data
                }, ensure_ascii=False))
            elif msg_type == "update_status":
                serial_number = request.get("serial_number")
                identifier = request.get("identifier")
                status = request.get("status")
                pdf_name = request.get("pdf_name")
                if not serial_number:
                    await websocket.send(json.dumps({
                        "type": "error",
                        "message": "缺少serial_number"
                    }, ensure_ascii=False))
                    continue
                upsert_order_status(
                    serial_number=str(serial_number),
                    identifier=str(identifier) if identifier is not None else "",
                    status=status,
                    pdf_name=pdf_name
                )
                await websocket.send(json.dumps({
                    "type": "update_ack",
                    "serial_number": serial_number,
                    "status": "ok"
                }, ensure_ascii=False))
            elif msg_type == "ping":
                await websocket.send(json.dumps({"type": "pong"}))
            else:
                await websocket.send(json.dumps({"type": "error", "message": f"未知消息类型: {msg_type}"}))
    finally:
        connected_clients.discard(websocket)

async def _broadcast_to_clients(payload: dict):
    if not connected_clients:
        return
    dead = []
    message = json.dumps({
        "type": "order_status_update",
        "data": build_record_payload(payload)
    }, ensure_ascii=False)
    for ws in connected_clients:
        try:
            await ws.send(message)
        except Exception:
            dead.append(ws)
    for ws in dead:
        connected_clients.discard(ws)

def broadcast_order_status(record: dict):
    global websocket_loop
    if not websocket_loop or not websocket_loop.is_running():
        return
    asyncio.run_coroutine_threadsafe(_broadcast_to_clients(record), websocket_loop)

def start_websocket_server():
    def _run():
        global websocket_loop
        websocket_loop = asyncio.new_event_loop()
        asyncio.set_event_loop(websocket_loop)
        server = websockets.serve(handle_websocket, WEBSOCKET_HOST, WEBSOCKET_PORT)
        websocket_loop.run_until_complete(server)
        print(f"✅ WebSocket服务已启动: ws://{WEBSOCKET_HOST}:{WEBSOCKET_PORT}")
        websocket_loop.run_forever()
    thread = threading.Thread(target=_run, daemon=True)
    thread.start()

def query_customer_name_and_price(customer_id, current_date):
    """
    从orders.db中根据编号查询客户信息
    
    查询逻辑：
    1. 通过编号（角标数字）在订单表中找到客户名称
    2. 通过客户名称在customers表中找到单价
    
    参数:
    customer_id: 编号（角标识别的数字，如210, 308等）
    current_date: 当前日期，格式为 YYYYMMDD（用于查找对应日期的订单表）
    
    返回:
    tuple: (客户名称, 单价) 或 (None, None) 如果未找到
    """
    # 如果编号无效，直接返回None
    if not customer_id or customer_id == 0:
        return None, None
        
    conn = None
    try:
        conn = get_db_connection()
        table_name = f"orders_{current_date}"
        
        cursor = conn.execute(
            f'SELECT customer FROM {table_name} WHERE number = ? LIMIT 1',
            (str(customer_id),)
        )
        order_result = cursor.fetchone()
        
        if order_result:
            customer_name = order_result['customer']
            
            cursor = conn.execute(
                'SELECT price FROM customers WHERE name = ? LIMIT 1',
                (customer_name,)
            )
            customer_result = cursor.fetchone()
            
            if customer_result:
                return customer_name, customer_result['price']
            else:
                return customer_name, None
        else:
            return None, None
                
    except sqlite3.Error as e:
        if 'no such table' not in str(e):
            print(f"⚠️ 订单查询错误: {e}")
        return None, None
    except Exception as e:
        print(f"⚠️ 数据库连接错误: {e}")
        return None, None
    finally:
        if conn:
            conn.close()

base_dir = ""
# 获取当前脚本所在目录
if getattr(sys, 'frozen', False):
    # 如果是打包后的exe
    base_dir = os.path.dirname(sys.executable)
else:
    # 如果是脚本运行
    base_dir = os.path.dirname(os.path.abspath(__file__))

端口号 = 11435

# 获取局域网IP地址
def get_local_ip():
    try:
        # 创建一个socket连接，用来获取本机的局域网IP
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        # 连接任意可达的外部地址，不会真正建立连接
        s.connect(("8.8.8.8", 80))
        # 获取本机的局域网IP
        ip = s.getsockname()[0]
        s.close()
        return ip
    except:
        return "127.0.0.1"  # 如果获取失败，返回本地回环地址

局域网IP = get_local_ip()
内网地址 = f"http://{局域网IP}:{端口号}"
二维码地址 = 内网地址  # 默认使用内网地址，后面会根据外网地址.txt内容决定最终使用哪个

# 读取或创建 外网地址.txt
外网地址文件路径 = os.path.join(base_dir, "外网地址.txt")
if os.path.exists(外网地址文件路径):
    # 文件存在，读取内容
    with open(外网地址文件路径, "r") as f:
        外网地址内容 = f.readlines()
        # 确保至少有一行
        if 外网地址内容 and 外网地址内容[0].strip():
            # 第1行有内容，作为外网地址
            外网地址 = 外网地址内容[0].strip()
            二维码地址 = 外网地址  # 优先使用外网地址
        else:
            # 第1行为空，使用内网地址
            外网地址 = ""
            二维码地址 = 内网地址
            
    # 重写文件，保留第1行，更新第2行为内网地址
    with open(外网地址文件路径, "w") as f:
        f.write(f"{外网地址}\n{内网地址}")
else:
    # 文件不存在，创建新文件
    外网地址 = ""
    with open(外网地址文件路径, "w") as f:
        f.write(f"\n{内网地址}")  # 第1行留空，第2行写入内网地址


# 设置PaddleOCR模型目录
# 优先查找外层的 paddleocr 目录，如果不存在则查找 _internal 目录
paddle_ocr_dir = os.path.join(base_dir, 'paddleocr')
if not os.path.exists(paddle_ocr_dir) and getattr(sys, 'frozen', False):
    # 如果是打包后的exe，尝试 _internal 目录
    paddle_ocr_dir = os.path.join(base_dir, 'paddleocr')
    print(f"使用 _internal 目录的模型: {paddle_ocr_dir}")
os.environ['PADDLE_OCR_BASE_DIR'] = paddle_ocr_dir

# 禁用OneDNN以解决兼容性问题
os.environ['FLAGS_use_mkldnn'] = '0'

# 设置PaddleOCR日志级别为ERROR，只显示错误信息
logging.getLogger('ppocr').setLevel(logging.ERROR)

ocr = PaddleOCR(
    # use_angle_cls=True,
    lang='ch',
    det_model_dir=os.path.join(paddle_ocr_dir, 'det'),  # 显式指定检测模型路径
    rec_model_dir=os.path.join(paddle_ocr_dir, 'rec'),  # 显式指定识别模型路径
    cls_model_dir=os.path.join(paddle_ocr_dir, 'cls'),  # 分类模型路径（可选）
    use_gpu=False,  # 明确使用CPU
    enable_mkldnn=False,  # 禁用MKL-DNN
    # det_limit_side_len=0,  # 设置为0表示不限制图像尺寸
    det_limit_type='max'   # 设置为'max'表示不进行缩放
) # need to run only once to download and load model into memory


def convert_pdf_to_binary_images(pdf_path, output_dir=None, dpis=[72]):
    """
    将PDF的每一页转换为无压缩的黑白二值图像，并保存为PNG文件。
    支持多个DPI和尺寸选项，解决细线条丢失问题。

    参数:
        pdf_path: PDF文件路径
        output_dir: 输出目录，默认为PDF所在目录
        dpis: DPI选项列表，默认[72]
    """
    # print("正在调用 convert_pdf_to_binary_images 函数！")
    # 新增数据结构存储像素尺寸
    pixel_size_data = {}
    
    # 确定输出目录
    if output_dir is None:
        output_dir = os.path.dirname(pdf_path)
    
    # 获取PDF文件名（不含扩展名）
    pdf_filename = os.path.splitext(os.path.basename(pdf_path))[0]
    
    # 打开PDF文件
    doc = fitz.open(pdf_path)
    
    # 处理每一页
    for page_num, page in enumerate(doc):
        # 获取页面尺寸（以点为单位）
        page_width_pt = page.rect.width
        page_height_pt = page.rect.height
        
        # 对每个DPI进行处理
        for dpi in dpis:
            # 渲染页面为图像，使用原始尺寸
            pix = page.get_pixmap(dpi=dpi, alpha=False, colorspace="GRAY", 
                                matrix=fitz.Matrix(1.0, 1.0))  # 1.0 表示不缩放

            # 将图像转换为PIL格式
            img = Image.frombytes("L", [pix.width, pix.height], pix.samples)

            # 将非黑白的颜色都设置为白色（调整阈值）
            # img = img.point(lambda p: 255 if p > 50 else 0)  # 将阈值从128调整为50

            # 二值化处理（严格黑白）
            threshold = 128  # 阈值
            img_binary = img.point(lambda p: 255 if p > threshold else 0)

            # 构造输出文件路径（已简化文件名，仅保留页码）
            output_image_path = os.path.join(output_dir, f"{pdf_filename}.png")
            
            # 保存为无压缩的PNG文件
            img_binary.save(output_image_path, format="PNG", compress_level=0)
            
            # 提取文件名作为key（不带扩展名）
            file_key = os.path.splitext(os.path.basename(output_image_path))[0]
            
            # 固定像素尺寸，72 DPI 下每个点对应一个像素
            mm_per_pixel = 25.4 / 72  # 1英寸=25.4毫米，72 DPI
            
            # 存储像素尺寸到字典
            pixel_size_data[file_key] = mm_per_pixel
            
            # print(f"第{page_num+1}页黑白图像已保存到: {output_image_path}")

    # 处理完当前PDF后保存数据到文件
    # 获取 output_dir 上一层目录
    script_dir = get_base_dir()
    # print(f"script_dir: {script_dir}")
    # output_file = os.path.join(script_dir, "像素尺寸.txt")
    
    # 如果文件已存在，先读取原有数据
    # if os.path.exists(output_file):
    #     with open(output_file, 'r', encoding='utf-8') as f:
    #         existing_data = json.load(f)
    #     # 更新原有数据
    #     existing_data.update(pixel_size_data)
    #     pixel_size_data = existing_data
    
    # # 写入文件（追加或新建）
    # with open(output_file, 'w', encoding='utf-8') as f:
    #     json.dump(pixel_size_data, f, ensure_ascii=False, indent=4)

def process_pdf_directory(pdf_dir, dpis=[72]):
    """
    处理指定目录中的所有PDF文件，转换每一页为黑白PNG图像（不包括子目录）
    
    参数:
        pdf_dir: 包含PDF文件的目录路径
        dpis: DPI选项列表
    """
    # print("正在调用 process_pdf_directory 函数！")
    
    # 检查目录是否存在
    if not os.path.exists(pdf_dir):
        print(f"PDF目录不存在: {pdf_dir}")
        return
    
    # 只遍历当前目录，不递归子目录
    files = os.listdir(pdf_dir)
    for file in files:
        if file.lower().endswith('.pdf'):
            pdf_path = os.path.join(pdf_dir, file)
            xlsx_path = os.path.join(pdf_dir, file.replace('.pdf', '.xlsx'))
            if os.path.exists(xlsx_path):
                # print(f"文件已存在: {xlsx_path}")
                pass
            else:
                # print(f"正在处理PDF: {pdf_path}")
                convert_pdf_to_binary_images(pdf_path, dpis=dpis)

def get_pdf_absolute_path():
    """
    获取PDF输入和输出目录的绝对路径
    
    优先级：
    1. 命令行参数 --input-dir 和 --output-dir
    2. path.ini 配置文件
    3. 默认路径 {脚本目录}/pdf
    
    返回:
    tuple: (输入目录, 输出目录)
    """
    input_dir = None
    output_dir = None
    
    if CMD_INPUT_DIR:
        input_dir = CMD_INPUT_DIR
        print(f"使用命令行参数的输入路径: {input_dir}")
        
        if CMD_OUTPUT_DIR:
            output_dir = CMD_OUTPUT_DIR
            print(f"使用命令行参数的输出路径: {output_dir}")
        else:
            # 如果只指定了输入目录，输出目录与输入目录相同
            output_dir = input_dir
            print(f"输出路径未指定，使用输入路径作为输出路径")
        
        return input_dir, output_dir
    
    return input_dir, output_dir

def run_script1():
    # print("正在运行1.py...")
    print(f"base_dir: {base_dir}")
    input_dir, output_dir = get_pdf_absolute_path()  # 从配置文件获取PDF文件夹路径
    print(f"输入目录: {input_dir}")
    print(f"输出目录: {output_dir}")
    # 可以根据需要自定义DPI
    dpis = [72]  # 仅使用72 DPI
    process_pdf_directory(input_dir, dpis=dpis)

def process_image(image_path, output_dir=None):
    """
    处理图像并将结果保存为带_result后缀的新文件
    
    参数:
        image_path: 输入图像路径
        output_dir: 输出目录（如果为None，则保存在输入图像同目录）
    """
    # 如果没有指定输出目录，使用输入图像所在目录
    if output_dir is None:
        output_dir = os.path.dirname(image_path)
    
    # 生成结果文件路径（保存到输出目录）
    filename = os.path.basename(image_path)
    result_filename = filename.replace('.png', '_result.png')
    result_filename_中文 = filename.replace('.png', '_result_方框裁剪.png')
    
    result_path = os.path.join(output_dir, result_filename)
    result_path_中文 = os.path.join(output_dir, result_filename_中文)
    
    # 确保输出目录存在
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    if "P5-25-2.23" not in image_path:
        # return result_path
        pass
    
    # 读取图像，增加错误检查
    with open(image_path, 'rb') as f:
        img_buffer = np.frombuffer(f.read(), dtype=np.uint8)
    img = cv2.imdecode(img_buffer, cv2.IMREAD_GRAYSCALE)
    if img is None:
        print(f"无法读取图像: {image_path}")
        return None
    
    # 二值化处理
    _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
    
    # 给binary一个独立的副本
    binary_copy = binary.copy()
    binary_origin = binary.copy()
    
    # 创建彩色图像用于标记
    marked_img = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR)
    
    # 获取图像尺寸
    height_binary, width_binary = binary.shape # 高y比宽x多
    
    黑框带图形的个数 = -1
    juxing_binary_list = [] # 存放黑框截图
    jiaobiao_binary_list = [] # 存放角标截图
    juxing_length_list = []  # 新增：存放黑框长边
    juxing_width_list = []   # 新增：存放黑框短边
    
    # 创建排除区域的掩码图（提前初始化）
    # 排除掩码 = np.zeros((height_binary, width_binary), dtype=np.uint8)
    排除掩码 = np.zeros((height_binary, width_binary), dtype=bool)
    
    for y in range(1, height_binary-1):
        for x in range(1, width_binary-1):
            # 使用掩码图快速判断排除区域（O(1)时间复杂度）
            if 排除掩码[y, x]:
                continue

            # 判断 marked_img[y, x] 是否为绿色或者蓝色
            # if np.array_equal(marked_img[y, x], [0, 255, 0]) or np.array_equal(marked_img[y, x], [255, 0, 0]):
            #     continue

            # 如果是白色像素
            if binary_copy[y, x] == 255:
                # 膨胀处理找到连通区域
                mask = np.zeros((height_binary+2, width_binary+2), np.uint8) # 用0填充周围一圈
                try:
                    cv2.floodFill(binary, mask, (x, y), 64)
                except Exception as e:
                    print(f"floodFill错误 | 当前坐标({x},{y})")
                    print(f"图像尺寸: {binary.shape} | mask尺寸: {mask.shape}")
                    print(f"错误详情: {str(e)}")
                    raise  # 重新抛出错误以便查看完整调用栈

                # 获取连通区域坐标
                coords = np.where(mask[1:-1, 1:-1] == 1)
                min_y, max_y = np.min(coords[0]), np.max(coords[0])
                min_x, max_x = np.min(coords[1]), np.max(coords[1])

                white_region_is_valid = False

                if (min_x > 0 and max_x < width_binary-1 and min_y > 0 and max_y < height_binary-1):
                    # 检查四条边是否全为黑色
                    left_edge_all_zero = np.all(binary_copy[min_y-1:max_y+2, min_x-1] == 0)
                    right_edge_all_zero = np.all(binary_copy[min_y-1:max_y+2, max_x+1] == 0)
                    top_edge_all_zero = np.all(binary_copy[min_y-1, min_x-1:max_x+2] == 0)
                    bottom_edge_all_zero = np.all(binary_copy[max_y+1, min_x-1:max_x+2] == 0)

                    # 调试信息：打印每条边的检查结果
                    if not left_edge_all_zero:
                        pass
                        # print(f"左边不全为0，黑色像素坐标：")
                        # 获取左边黑色像素的y坐标
                        # y_coords = np.argwhere(binary_copy[min_y-1:max_y+2, min_x-1] != 0)
                        # 打印x和y坐标
                        # for y in y_coords: print(f"({min_x-1}, {min_y-1 + y[0]})")
                    if not right_edge_all_zero:
                        pass
                        # print(f"右边不全为0，黑色像素坐标：{np.argwhere(binary_copy[min_y-1:max_y+2, max_x+1] != 0)}")
                    if not top_edge_all_zero:
                        pass
                        # print(f"上边不全为0，黑色像素坐标：{np.argwhere(binary_copy[min_y-1, min_x-1:max_x+2] != 0)}")
                    if not bottom_edge_all_zero:
                        pass
                        # print(f"下边不全为0，黑色像素坐标：{np.argwhere(binary_copy[max_y+1, min_x-1:max_x+2] != 0)}")
                    # 如果上面4个条件只有1个为False，说明遇到异常，则把改边外拓1个像素，如果该外拓边满足全黑则继续，把binary_copy和binary_origin该边设置为黑色
                    # 如果只有一条边不满足全黑条件
                    if sum([left_edge_all_zero, right_edge_all_zero, top_edge_all_zero, bottom_edge_all_zero]) == 3:
                        # 处理左边不全为黑的情况
                        if not left_edge_all_zero:
                            # 外拓1个像素
                            left_edge_all_zero = np.all(binary_copy[min_y-1:max_y+2, min_x-2] == 0)
                            if left_edge_all_zero:
                                # 将binary_copy和binary_origin的左边设置为黑色
                                binary_copy[min_y-1:max_y+2, min_x-1] = 0
                                binary_origin[min_y-1:max_y+2, min_x-1] = 0
                        # 处理右边不全为黑的情况
                        elif not right_edge_all_zero:
                            # 外拓1个像素
                            right_edge_all_zero = np.all(binary_copy[min_y-1:max_y+2, max_x+2] == 0)
                            if right_edge_all_zero:
                                # 将binary_copy和binary_origin的右边设置为黑色
                                binary_copy[min_y-1:max_y+2, max_x+1] = 0
                                binary_origin[min_y-1:max_y+2, max_x+1] = 0
                        # 处理上边不全为黑的情况
                        elif not top_edge_all_zero:
                            # 外拓1个像素
                            top_edge_all_zero = np.all(binary_copy[min_y-2, min_x-1:max_x+2] == 0)
                            if top_edge_all_zero:
                                # 将binary_copy和binary_origin的上边设置为黑色
                                binary_copy[min_y-1, min_x-1:max_x+2] = 0
                                binary_origin[min_y-1, min_x-1:max_x+2] = 0
                        # 处理下边不全为黑的情况
                        elif not bottom_edge_all_zero:
                            # 外拓1个像素
                            bottom_edge_all_zero = np.all(binary_copy[max_y+2, min_x-1:max_x+2] == 0)
                            if bottom_edge_all_zero:
                                # 将binary_copy和binary_origin的下边设置为黑色
                                binary_copy[max_y+1, min_x-1:max_x+2] = 0
                                binary_origin[max_y+1, min_x-1:max_x+2] = 0

                # 判断是否所有边都全为黑色
                if (min_x > 0 and max_x < width_binary-1 and min_y > 0 and max_y < height_binary-1 and 
                    left_edge_all_zero and
                    right_edge_all_zero and 
                    top_edge_all_zero and
                    bottom_edge_all_zero):


                    # # 判断marked_img[min_y:max_y+1, min_x:max_x+1] 4个角的5*5像素是否要么是黑色要么是绿色，如果不是则continue
                    # 检测范围 = 15 # 检测范围
                    # # 检查四个角的5x5像素区域
                    # top_left = marked_img[min_y:min_y+检测范围, min_x:min_x+检测范围]
                    # top_right = marked_img[min_y:min_y+检测范围, max_x-4:max_x+1]
                    # bottom_left = marked_img[max_y-4:max_y+1, min_x:min_x+检测范围]
                    # bottom_right = marked_img[max_y-4:max_y+1, max_x-4:max_x+1]
                    # #
                    # corners_valid = False
                    # if ([0,255,0] in top_left) or ([0,255,0] in top_right) or ([0,255,0] in bottom_left) or ([0,255,0] in bottom_right):
                    #     # 标记为蓝色并设置排除掩码
                    #     marked_img[coords[0], coords[1]] = [255, 0, 0]  # 蓝色
                    #     # cv2.imwrite(result_path_中文, marked_img) # 调试
                    #     if 调试模式:
                    #         cv2.imencode('.png', marked_img)[1].tofile(result_path_中文) # TODO
                    #     排除掩码[coords[0], coords[1]] = True # 这部分区域不参与查找了
                    #     continue

                    # marked_img coords部分设置为绿色
                    marked_img[coords[0], coords[1]] = (0, 255, 0)  # 标记绿色
                    if 调试模式:
                        cv2.imencode('.png', marked_img)[1].tofile(result_path_中文) # TODO
                    pass

                    检测范围 = 15 # 检测范围
                    # 检查四个角的5x5像素区域
                    top_left = marked_img[min_y:min_y+检测范围, min_x:min_x+检测范围]
                    top_right = marked_img[min_y:min_y+检测范围, max_x-4:max_x+1]
                    bottom_left = marked_img[max_y-4:max_y+1, min_x:min_x+检测范围]
                    bottom_right = marked_img[max_y-4:max_y+1, max_x-4:max_x+1]
                    #
                    corners_valid = False
                    if [0,255,0] in top_left and [0,255,0] in top_right and [0,255,0] in bottom_left and [0,255,0] in bottom_right:
                        # print("4个角都有绿色，有效矩形区域")
                        pass
                    else:
                        # 标记为蓝色并设置排除掩码
                        marked_img[coords[0], coords[1]] = [255, 0, 0]  # 蓝色
                        # cv2.imwrite(result_path_中文, marked_img) # 调试
                        if 调试模式:
                            cv2.imencode('.png', marked_img)[1].tofile(result_path_中文) # TODO
                        排除掩码[coords[0], coords[1]] = True # 这部分区域不参与查找了
                        continue
                        

                    # 处理有效矩形区域
                    排除掩码[min_y-1:max_y+2, min_x-1:max_x+2] = True 
                    
                    ##############################################################
                    # 提取客户角标，提取到了就表示有效黑框也是有效图形
                    # 识别角标

                    if max_y - min_y < 10 or max_x - min_x < 10: # 异常
                        white_region_is_valid = False
                        binary_copy[min_y:max_y+1, min_x:max_x+1] = 0 # 标记黑色
                        # 使用cv2.rectangle绘制3像素宽度的红色边框
                        # cv2.rectangle(marked_img, (min_x, min_y), (max_x, max_y), (0, 0, 255), 3)  # 标记红色
                        # cv2.imwrite(result_path_中文, marked_img) # 调试用
                        if 调试模式:
                            cv2.imencode('.png', marked_img)[1].tofile(result_path_中文) # TODO
                        continue # 忽略窄边空白异常

                    # !!! binary_origin
                    juxing_binary = binary_origin[min_y:max_y+1, min_x:max_x+1].copy()
                    # 保存 juxing_binary 图像
                    # cv2.imwrite(result_path_中文.replace('.png', '_binary_temp.png'), binary)
                    # cv2.imwrite(result_path_中文.replace('.png', '_juxing_binary_temp.png'), juxing_binary)
                    pass

                    # 把 juxing_binary 4个边缘2像素宽度设置为白色
                    juxing_binary[:2, :] = 255  # 上边缘
                    juxing_binary[-2:, :] = 255  # 下边缘
                    juxing_binary[:, :2] = 255  # 左边缘
                    juxing_binary[:, -2:] = 255  # 右边缘
                    
                    
                    jiaobiao_height, jiaobiao_width = juxing_binary.shape

                    # 将矩形分为四个区域
                    top_left = juxing_binary[:jiaobiao_height//2, :jiaobiao_width//2]  # 左上
                    top_right = juxing_binary[:jiaobiao_height//2, jiaobiao_width//2:]  # 右上
                    bottom_left = juxing_binary[jiaobiao_height//2:, :jiaobiao_width//2]  # 左下
                    bottom_right = juxing_binary[jiaobiao_height//2:, jiaobiao_width//2:]  # 右下
                    
                    # 对各个区域进行旋转
                    top_left = top_left # 不旋转
                    top_right = cv2.rotate(top_right, cv2.ROTATE_90_COUNTERCLOCKWISE)  # 右上逆时针90度
                    bottom_right = cv2.rotate(bottom_right, cv2.ROTATE_180)  # 右下逆时针180度
                    bottom_left = cv2.rotate(bottom_left, cv2.ROTATE_90_CLOCKWISE)  # 左下顺时针90度
                    # 上面4个并排显示下 imshow,缩放一半，防止太大
                    if 调试模式:
                        # 创建一个水平拼接的图像用于显示四个角区域
                        max_h = max(top_left.shape[0], top_right.shape[0], bottom_left.shape[0], bottom_right.shape[0])
                        max_w = max(top_left.shape[1], top_right.shape[1], bottom_left.shape[1], bottom_right.shape[1])
                        
                        # 调整所有图像到相同大小
                        top_left_resized = cv2.resize(top_left, (max_w//2, max_h//2))
                        top_right_resized = cv2.resize(top_right, (max_w//2, max_h//2))
                        bottom_left_resized = cv2.resize(bottom_left, (max_w//2, max_h//2))
                        bottom_right_resized = cv2.resize(bottom_right, (max_w//2, max_h//2))
                        
                        # 水平拼接上部和下部
                        top_row = np.hstack((top_left_resized, top_right_resized))
                        bottom_row = np.hstack((bottom_left_resized, bottom_right_resized))
                        
                        # 垂直拼接上下部分
                        corners_display = np.vstack((top_row, bottom_row))
                        
                        # 显示拼接后的图像
                        # cv2.imshow('四个角区域', corners_display)
                        # cv2.waitKey(0)  # 等待按任意键
                        # cv2.destroyAllWindows()
                    
                    # 初始化列表
                    zhijiao_binary_list = [None] * 4
                    zhijiao_binary_list = [top_left, top_right, bottom_right, bottom_left]
                    # 保存角标参数                    
                    zhijiao_canshu_list = [None] * 4
                    for idx in range(4): # 遍历四个角区域
                        # 当前zhijiao_binary的高宽
                        jiaobiao_height, jiaobiao_width = zhijiao_binary_list[idx].shape
                        x_start_出现黑色像素 = 0
                        x_end_出现黑色像素 = x_start_出现黑色像素
                        for x in range(1,jiaobiao_width): # 0~jiaobiao_width-1
                            # x所在列存在黑色像素，只要有一个黑色像素就加一
                            if np.any(zhijiao_binary_list[idx][1:, x] == 0):
                                x_start_出现黑色像素 = x
                                break
                        if x_start_出现黑色像素 == 0:
                            # print("该角落未找到角标！")
                            zhijiao_canshu_list[idx] = [-1, -1, -1, -1, 2]
                            continue # 跳过当前1/4角落
                        #
                        y_start_出现黑色像素 = 0
                        y_end_出现黑色像素 = y_start_出现黑色像素
                        for y in range(1,jiaobiao_height): # 0~jiaobiao_height-1
                            # y所在行存在黑色像素，只要有一个黑色像素就加一
                            if np.any(zhijiao_binary_list[idx][y, 1:] == 0):
                                y_start_出现黑色像素 = y
                                break
                        ###################################
                        # 计算连续黑色像素的起始位置和结束位置
                        角标距离阈值 = min(50,jiaobiao_height-y_start_出现黑色像素)
                        for x in range(x_start_出现黑色像素, jiaobiao_width): # x_start_出现黑色像素~jiaobiao_width-1
                            if np.any(zhijiao_binary_list[idx][1:y_start_出现黑色像素+角标距离阈值, x] == 0):
                               continue
                            else:
                                x_end_出现黑色像素 = x
                                break
                        #
                        角标距离阈值 = min(50,jiaobiao_width-x_start_出现黑色像素)
                        for y in range(y_start_出现黑色像素, jiaobiao_height): # y_start_出现黑色像素~jiaobiao_height-1
                            if np.any(zhijiao_binary_list[idx][y, 1:x_start_出现黑色像素+角标距离阈值] == 0):
                                continue
                            else:
                                y_end_出现黑色像素 = y
                                break
                        zhijiao_canshu_list[idx] = [x_start_出现黑色像素, x_end_出现黑色像素, y_start_出现黑色像素, y_end_出现黑色像素, x_start_出现黑色像素**2+y_start_出现黑色像素**2] # 保存角标参数
                    # 在一行里打印 zhijiao_canshu_list 每个列表元素的第5个元素
                    # print("各角标参数平方和:", [item[4] for item in zhijiao_canshu_list])


                    # 判断是否找到有效角标逻辑1
                    # 比较 zhijiao_canshu_list 中每个元素的第4个值，取最小值，对应的 索引
                    # 排除 zhijiao_canshu_list[i][0] == -1 的元素
                    valid_indices = [i for i in range(len(zhijiao_canshu_list)) if zhijiao_canshu_list[i][0] != -1]
                    if valid_indices:  # 如果有有效元素
                        # 计算角标距离直角顶点最短的那个直角的判断为角标
                        min_index = min(valid_indices, key=lambda i: zhijiao_canshu_list[i][4])
                    else:  # 如果所有元素都是 -1
                        min_index = -1  # 返回一个无效索引
                    # 判断是否找到有效角标逻辑2
                    if zhijiao_canshu_list[min_index][1] < zhijiao_canshu_list[min_index][0] or zhijiao_canshu_list[min_index][3] < zhijiao_canshu_list[min_index][2]:
                        min_index = -1  # 返回一个无效索引


                    if min_index == -1:
                        # print("该角落未找到角标！")
                        white_region_is_valid = False
                        binary_copy[min_y:max_y+1, min_x:max_x+1] = 0
                        marked_img[min_y:max_y+1, min_x:max_x+1] = (125, 0, 0) # 标记蓝色
                        # cv2.imwrite(result_path_中文, marked_img) # 调试用
                        if 调试模式:
                            cv2.imencode('.png', marked_img)[1].tofile(result_path_中文) # TODO
                        pass
                    else:
                        white_region_is_valid = True
                        juxing_binary_list.append(juxing_binary)
                        # 计算长宽并存储
                        h, w = juxing_binary.shape
                        length = max(h, w)
                        width = min(h, w)
                        juxing_length_list.append(length)
                        juxing_width_list.append(width)
                        黑框带图形的个数 += 1
                        print("正在处理第{}个带有效图形黑框".format(黑框带图形的个数))
                        if 调试模式:
                            cv2.imencode('.png', marked_img)[1].tofile(result_path_中文)
                        pass

                        # 保存 zhijiao_binary_list[min_index]
                        zhijiao_binary_list_element_path =image_path.replace('.png', '_result_zhijiao_binary_'+str(黑框带图形的个数)+'_suoyin'+'.png')
                        tmppath = zhijiao_binary_list_element_path
                        tmppath = os.path.join(os.path.dirname(tmppath), tmppath.split('_result')[0], os.path.basename(tmppath))
                        zhijiao_binary_list_element_path = tmppath
                        # cv2.imwrite(zhijiao_binary_list_element_path.replace('suoyin','0'), zhijiao_binary_list[0])
                        # cv2.imwrite(zhijiao_binary_list_element_path.replace('suoyin','1'), zhijiao_binary_list[1])
                        # cv2.imwrite(zhijiao_binary_list_element_path.replace('suoyin','2'), zhijiao_binary_list[2])
                        # cv2.imwrite(zhijiao_binary_list_element_path.replace('suoyin','3'), zhijiao_binary_list[3])
                        
                        # cv2.imwrite("tmptmptmp_juxing.png", juxing_binary)
                        # cv2.imwrite("tmptmptmp_jiaobiao.png", zhijiao_binary_list[min_index])

                        # 计算角标所在的包络区域
                        if zhijiao_canshu_list[min_index][1] - zhijiao_canshu_list[min_index][0] > zhijiao_canshu_list[min_index][3] - zhijiao_canshu_list[min_index][2]:
                            # x方向出现连续黑色像素，说明x方向是数字高度方向，应该顺时间旋转90度
                            x = zhijiao_canshu_list[min_index][1]
                            y = zhijiao_canshu_list[min_index][2] + (zhijiao_canshu_list[min_index][1] - zhijiao_canshu_list[min_index][0]) * 3 # 假设1个数字占据一个数字高度的方块
                            jiaobiao_binary = zhijiao_binary_list[min_index][zhijiao_canshu_list[min_index][2]:y, zhijiao_canshu_list[min_index][0]:x]
                            # 把 jiaobiao_binary 顺时间旋转90度
                            jiaobiao_binary = cv2.rotate(jiaobiao_binary, cv2.ROTATE_90_CLOCKWISE)
                        if zhijiao_canshu_list[min_index][1] - zhijiao_canshu_list[min_index][0] <= zhijiao_canshu_list[min_index][3] - zhijiao_canshu_list[min_index][2]:
                            # y方向出现连续黑色像素，说明y方向是数字高度方向，方向正常
                            x = zhijiao_canshu_list[min_index][0] + (zhijiao_canshu_list[min_index][3] - zhijiao_canshu_list[min_index][2]) * 3
                            y = zhijiao_canshu_list[min_index][3]
                            jiaobiao_binary = zhijiao_binary_list[min_index][zhijiao_canshu_list[min_index][2]:y, zhijiao_canshu_list[min_index][0]:x]
                        # 
                        tmppath = image_path.replace('.png', '_result_jiaobiao_binary_'+str(黑框带图形的个数)+'.png')
                        tmppath = os.path.join(os.path.dirname(tmppath), tmppath.split('_result')[0], os.path.basename(tmppath))
                        # cv2.imwrite(tmppath, jiaobiao_binary)
                        
                        # 把 jiaobiao_binary 加到 jiaobiao_binary_list 等待拼接
                        jiaobiao_binary_list.append(jiaobiao_binary)
                        
                        # jiaobiao_binary 保存角标所在包络区域
                        jiaobiao_height, jiaobiao_width = jiaobiao_binary.shape                        
                        # 你的图像处理代码
                        h, w = jiaobiao_binary.shape
                        output_path = image_path.replace('.png', '_result_jiaobiao_binary_'+str(黑框带图形的个数)+'.png')
                        tmppath = output_path
                        tmppath = os.path.join(os.path.dirname(tmppath), tmppath.split('_result')[0], os.path.basename(tmppath))
                        output_path = tmppath
                        # cv2.imwrite(output_path, jiaobiao_binary)
                    ##############################################################
                    continue
                else:# !!!强制约束，图形必须在矩形方框内，不考虑非矩形的情况
                    # 非矩形的不考虑了，所以检测到非矩形，就肯定不是有效矩形
                    # print("检测到非矩形包络的白色联通区域！肯定无效！")

                    # 在检查矩形包络之前，先标记4条边为红色
                    try:
                        marked_img[min_y-1:max_y+2, min_x-1] = (0, 0, 255)  # 左边
                        marked_img[min_y-1:max_y+2, max_x+1] = (0, 0, 255)  # 右边
                        marked_img[min_y-1, min_x-1:max_x+2] = (0, 0, 255)  # 上边
                        marked_img[max_y+1, min_x-1:max_x+2] = (0, 0, 255)  # 下边
                        # 将标记后的图像保存为临时文件
                        # tmppath = image_path.replace('.png', '_result_binary_copy'+'.png')
                        # tmppath = os.path.join(os.path.dirname(tmppath), tmppath.split('_result')[0], os.path.basename(tmppath))
                        # cv2.imwrite(tmppath, marked_img)
                        pass
                    except:
                        # print(f"保存临时文件失败 | 当前坐标({x},{y})")
                        pass

                    binary_copy[coords[0], coords[1]] = 0
                    marked_img[coords[0], coords[1]] = (255, 0, 0) # 蓝色
                    # cv2.imwrite(result_path_中文, marked_img) # 调试用
                    if 调试模式:
                        cv2.imencode('.png', marked_img)[1].tofile(result_path_中文) # TODO
                    pass
                    continue
    
    cv2.imencode('.png', marked_img)[1].tofile(result_path_中文) # TODO 最后保存一下即可
    # 返回多个变量，使用字典形式
    return {
        'image_path': image_path, # 原始pdf图像
        'result_path': result_path, # 图绿/蓝的掩码图像
        'juxing_binary_list': juxing_binary_list, # 有效黑框图像列表
        'jiaobiao_binary_list': jiaobiao_binary_list, # 扩展到100x100的角标图像列表
        'juxing_length_list': juxing_length_list,  # 新增
        'juxing_width_list': juxing_width_list,    # 新增
    }

def process_png_directory(input_dir, output_dir=None):
    """
    处理指定目录中的所有PNG文件
    
    参数:
        input_dir: 输入目录（监控PDF/PNG文件的目录）
        output_dir: 输出目录（保存结果的目录，默认与输入目录相同）
    """
    if output_dir is None:
        output_dir = input_dir
    
    processed_count = 0
    
    print(f"开始处理PNG目录:")
    print(f"  输入目录: {input_dir}")
    print(f"  输出目录: {output_dir}")
    
    # 检查输入目录是否存在
    if not os.path.exists(input_dir):
        print(f"输入目录不存在: {input_dir}")
        return processed_count
    
    # 确保输出目录存在
    if not os.path.exists(output_dir):
        print(f"输出目录不存在，正在创建: {output_dir}")
        os.makedirs(output_dir, exist_ok=True)
    
    # 只获取输入目录中的文件
    files = os.listdir(input_dir)
    print(f"输入目录中共有 {len(files)} 个文件")
    
    # 筛选PNG文件
    png_files = [f for f in files if '_' not in f and f.lower().endswith('.png')]
    print(f"找到 {len(png_files)} 个符合条件的PNG文件: {png_files}")
    
    # 遍历文件
    for i in range(1):
        for file in files:
            if '_' not in file and file.lower().endswith('.png'):
                png_path = os.path.join(input_dir, file)
                print(f"准备处理PNG文件: {png_path}")
            else:
                continue

            try:
                print(f"开始处理: {png_path}")
                
                # 检查输出目录中是否已存在xlsx
                output_filename = os.path.basename(png_path).replace('.png', '.xlsx')
                xlsx_path = os.path.join(output_dir, output_filename)
                
                if os.path.exists(xlsx_path):
                    print(f"跳过文件: {png_path} (输出目录已存在 {xlsx_path})")
                    continue

                # 为每个PDF文件生成二维码（保存到输出目录）
                qr_filename = os.path.basename(png_path).replace('.png', '_result_qr.svg')
                qr_file = os.path.join(output_dir, qr_filename)
                
                pdffilename = os.path.basename(png_path)
                if pdffilename.lower().endswith('.png'): 
                    pdffilename = pdffilename[:-4]  # 删除.png后缀
                
                # 提取pdf页码 P3-25-0324，其中3是页码
                pagenum = pdffilename.split("-")[0].replace("P", "").replace("p", "")
                # 使用清理后的PDF文件名生成URL
                url = 二维码地址 + "/baogong?pdffilename=" + pdffilename
                # 生成二维码
                QR_Single_Code(link=url, pagenum=str(pagenum), output_file=qr_file, font_size=50)

                result = process_image(png_path, output_dir)
                if result is not None:  # 只有成功处理时才计数
                    processed_count += 1
                    # 拼接 jiaobiao_binary_list 依次从上到下垂直到 jiaobiao_binary_list_拼接
                    image_path = result['image_path']
                    print(f"image_path = '{image_path}'")
                    图像文件名 = os.path.splitext(os.path.basename(image_path))[0]
                    
                    # 创建以 图像文件名 为名的文件夹（在输出目录中）
                    中间图像输出文件夹 = os.path.join(output_dir, 图像文件名)
                    中间图像输出文件夹_temp = os.path.join(中间图像输出文件夹, "temp")
                    # 删除 中间图像输出文件夹_temp 下的 png 文件
                    # if os.path.exists(中间图像输出文件夹_temp):
                    #     for file in os.listdir(中间图像输出文件夹_temp):
                    #         if file.lower().endswith('.png'):
                    #             os.remove(os.path.join(中间图像输出文件夹_temp, file))
                    # 创建 中间图像输出文件夹_temp 文件夹
                    # os.makedirs(中间图像输出文件夹_temp, exist_ok=True)
                    # os.makedirs(中间图像输出文件夹, exist_ok=True)
                    # 
                    script_dir = get_base_dir()
                    像素尺寸_txt_path = os.path.join(script_dir, "像素尺寸.txt")
                    # print(f"像素尺寸_txt_path = '{像素尺寸_txt_path}'")

                    import json
                    # with open('像素尺寸.txt', 'r', encoding='utf-8') as f:
                    #     pixel_data = json.load(f)
                    # 像素尺寸 = pixel_data.get(图像文件名)  # 直接通过文件名key获取对应值
                    像素尺寸 = 0.3527
                    result_path = result['result_path']
                    print(f"result_path = '{result_path}'")
                    juxing_binary_list = result['juxing_binary_list']
                    jiaobiao_binary_list = result['jiaobiao_binary_list']
                    juxing_length_list = result['juxing_length_list']  # 新增
                    juxing_width_list = result['juxing_width_list']    # 新增
                    # juxing_length_mm_list 等于 juxing_length_list 每个元素乘以 像素尺寸
                    juxing_length_mm_list = [(length + 2) * 像素尺寸 for length in juxing_length_list]  # 新增
                    juxing_width_mm_list = [(width + 2) * 像素尺寸 for width in juxing_width_list]    # 新增
                    # mianji_mm_list 等于 juxing_length_mm_list 和 juxing_width_mm_list 对应元素相乘
                    mianji_mm_list = [length * width for length, width in zip(juxing_length_mm_list, juxing_width_mm_list)]  # 新增
                    

                    # 正则化 jiaobiao_binary_list
                    for idx, jiaobiao_binary in enumerate(jiaobiao_binary_list):
                        # 先收缩图像，去除四周全白的边缘
                        # 获取当前角标图像的高度和宽度
                        jiaobiao_height, jiaobiao_width = jiaobiao_binary.shape

                        # 计算上边收缩量
                        top = 0
                        while top < jiaobiao_height and np.all(jiaobiao_binary[top, :] == 255):
                            top += 1
                        # 计算下边收缩量
                        bottom = jiaobiao_height - 1
                        while bottom >= 0 and np.all(jiaobiao_binary[bottom, :] == 255):
                            bottom -= 1
                        # 计算左边收缩量
                        left = 0
                        while left < jiaobiao_width and np.all(jiaobiao_binary[:, left] == 255):
                            left += 1
                        # 计算右边收缩量
                        right = jiaobiao_width - 1
                        while right >= 0 and np.all(jiaobiao_binary[:, right] == 255):
                            right -= 1
                        
                        # 裁剪图像
                        jiaobiao_binary = jiaobiao_binary[top:bottom+1, left:right+1]
                        # 用裁剪过的图像替换当前元素
                        jiaobiao_binary_list[idx] = jiaobiao_binary
                    # 先遍历 jiaobiao_binary_list 所有图像元素的 高 宽，计算出最大的高宽
                    max_height = max(img.shape[0] for img in jiaobiao_binary_list)
                    max_width = max(img.shape[1] for img in jiaobiao_binary_list)
                    # 再遍历 jiaoobiao_binary_list 所有图像元素，放大到最大高宽
                    try:
                        # 处理图像 c:\code\jiedan\1000\shibiejiaobiao\pdf\P4-25-0301_page1.png 时出错: OpenCV(4.11.0) D:\a\opencv-python\opencv-python\opencv\modules\imgproc\src\resize.cpp:4208: error: (-215:Assertion failed) !ssize.empty() in function 'cv::resize'
                        # 遍历 jiaobiao_binary_list 所有图像元素，如果出现 宽或高为0，则用 100x100 的图像，图像是白色背景居中是1个宽50高25的黑色块
                        for idx, img in enumerate(jiaobiao_binary_list):
                            if img.shape[0] == 0 or img.shape[1] == 0:
                                # 创建100x100的白色背景图像
                                new_img = np.ones((100, 100), dtype=np.uint8) * 255
                                # 计算黑色块的起始位置
                                start_x = (100 - 50) // 2
                                start_y = (100 - 25) // 2
                                # 绘制黑色块
                                new_img[start_y:start_y+25, start_x:start_x+50] = 0
                                # 替换原图像
                                jiaobiao_binary_list[idx] = new_img
                        # jiaobiao_binary_list = [cv2.resize(img, (max_width, max_height), interpolation=cv2.INTER_NEAREST) for img in jiaobiao_binary_list]
                    except:
                        print(f"cv2.resize() 失败！")
                    # 拼接 jiaobiao_binary_list
                    for idx, jiaobiao_binary in enumerate(jiaobiao_binary_list):
                        jiaobiao_height, jiaobiao_width = jiaobiao_binary.shape
                        
                        # 计算需要的填充量
                        pad_h = max(0, 100 - jiaobiao_height)
                        pad_w = max(0, 100 - jiaobiao_width)
                        # 均匀填充白色（255）
                        jiaobiao_binary = cv2.copyMakeBorder(jiaobiao_binary, 
                                                        pad_h // 2, pad_h - pad_h // 2,
                                                        pad_w // 2, pad_w - pad_w // 2,
                                                        cv2.BORDER_CONSTANT, value=255)
                        # 调整尺寸为100x100
                        jiaobiao_binary = cv2.resize(jiaobiao_binary, (100, 100), interpolation=cv2.INTER_NEAREST)
                        #
                        jiaobiao_binary_list[idx] = jiaobiao_binary
                        # 保存到文件
                        jiaobiao_binary_path = image_path.replace('.png', '_result_jiaobiao_binary_'+str(idx)+'.png')
                        tmppath = jiaobiao_binary_path
                        tmppath = os.path.join(os.path.dirname(tmppath), tmppath.split('_result')[0], os.path.basename(tmppath))
                        jiaobiao_binary_path = tmppath
                        # cv2.imwrite(jiaobiao_binary_path, jiaobiao_binary)
                        pass

                    for i in range(1): # 循环2次，不要循环，反而出错
                        concatenated_path = None  # 先初始化变量
                        # 计算拼接后的总高度和宽度
                        total_height = sum(img.shape[0] for img in jiaobiao_binary_list)
                        max_width = max(img.shape[1] for img in jiaobiao_binary_list)

                        # 创建空白画布用于纵向拼接
                        vertical_concatenated_img = np.ones((total_height, max_width), dtype=np.uint8) * 255
                        # 创建空白画布用于横向拼接
                        horizontal_concatenated_img = np.ones((max(img.shape[0] for img in jiaobiao_binary_list), sum(img.shape[1] for img in jiaobiao_binary_list)), dtype=np.uint8) * 255
                        
                        jiaobiao_binary_list_去除非数字 = jiaobiao_binary_list.copy()  # 复制一份，防止修改原列表
                        # 遍历 jiaobiao_binary_list_去除非数字，检测图像元素上的黑色联通区域，如果不等于3则替换为 ling_binary.png
                        
                        # 从上到下依次拼接（纵向）
                        y_offset = 0
                        for img in jiaobiao_binary_list:
                            h, w = img.shape
                            vertical_concatenated_img[y_offset:y_offset+h, :w] = img
                            y_offset += h

                        # 从左到右依次拼接（横向）
                        x_offset = 0
                        for img in jiaobiao_binary_list:
                            w, h = img.shape
                            horizontal_concatenated_img[:h, x_offset:x_offset+w] = img
                            x_offset += w
                        # 把纵向拼接的图赋值给 concatenated_img，竖着识别率似乎更高
                        concatenated_img = vertical_concatenated_img
                        # 把横向拼接的图赋值给 concatenated_img
                        # concatenated_img = horizontal_concatenated_img

                        

                        # 保存拼接后的图像（保存到output_dir）
                        concatenated_filename = os.path.basename(image_path).replace('.png', '_result_角标裁剪.png')
                        concatenated_path = os.path.join(output_dir, concatenated_filename)
                        print(f"concatenated_path = '{concatenated_path}'") 
                        # cv2.imwrite(concatenated_path, concatenated_img)
                        # cv2.imencode('.png', concatenated_img)[1].tofile(concatenated_path) # TODO
                        
                        jiaobiao_binary_shibie_list = [] # 存放识别到的客户名
                        jiaobiao_binary_shibie_true_false_list = []
                        jiaobiao_binary_shibie_备注_list = [] # 存放备注信息（如G）
                        角标图像个数 = len(jiaobiao_binary_list)
                        for i in range(角标图像个数): 
                            jiaobiao_binary_shibie_list.append(0)
                            jiaobiao_binary_shibie_true_false_list.append(False)
                            jiaobiao_binary_shibie_备注_list.append("")  # 初始化备注为空
                        
                        # 识别方法选择 = 1
                        识别方法选择 = 2 # 分组识别
                        if 识别方法选择 == 1:
                            # 识别方法1：调用kimi的API
                            with open(concatenated_path, "rb") as f:
                                image_data = f.read()
                            # 识别 jiaobiao_binary 作为客户角标 
                            client = OpenAI(
                                # api_key=os.environ.get("MOONSHOT_API_KEY"),
                                api_key="sk-GiryKJj05QLgVJnhCdDFqWKniaJnxeP5xdLvMUICG0ASgbgw", # 客户的
                                # api_key="sk-BWx7TgU5NhTyvPVfWFMWjIkK61Kr1wTu2PjJndEAcB3v78TJ", # 我的
                                base_url="https://api.moonshot.cn/v1",
                            )
                            #
                            image_url = f"data:image/png;base64,{base64.b64encode(image_data).decode('utf-8')}"
                            recognized_text = ""  # 先定义变量
                            while True:  # 添加无限重试循环
                                try:  # 尝试调用API
                                    completion = client.chat.completions.create(
                                        model="moonshot-v1-8k-vision-preview",
                                        messages=[
                                            # {
                                            #     "role": "system",
                                            #     "content": "你是Kimi，擅长准确识别图像中的文字内容。请专注提取文字，不要添加其他描述。"
                                            # },
                                            {
                                                "role": "user",
                                                "content": [
                                                    {
                                                        "type": "image_url",
                                                        "image_url": {"url": image_url}
                                                    },
                                                    {
                                                        "type": "text",
                                                        # "text": "识别每行内容，一共"+str(len(jiaobiao_binary_list))+"行，"+"输出json格式，只存储识别到的内容"
                                                        # "text": "识别图中数字，输出json格式，json使用列表类型，只存储识别到的内容"
                                                        "text": "识别图中数字，输出json格式，必须包含一个'numbers'键，其值为识别到的数字列表。示例：{'numbers': [123, 456, 789]}"
                                                    }
                                                ]
                                            }
                                        ],
                                        temperature=0.1
                                    )
                                    recognized_text = completion.choices[0].message.content  # 获取识别结果
                                    break  # 如果成功则退出循环
                                except Exception as e:
                                    print(f"API调用频率过高，请等待5秒: {str(e)}")
                                    time.sleep(5) # 每次重试前等待6秒
                                    continue # 其他错误也继续循环重试
                            print("原始文本:", recognized_text)
                            
                            # 提取JSON格式的识别结果
                            
                            try:
                                # 识别recognized_text方法序号 = 1
                                识别recognized_text方法序号 = 2
                                if 识别recognized_text方法序号 == 1:
                                    import json
                                    # 去除多余的字符
                                    json_str = recognized_text.strip().replace('```json', '').replace('```', '').strip()
                                    # 解析JSON
                                    jiaobiao_binary_shibie_list = json.loads(json_str) # 
                                    if isinstance(jiaobiao_binary_shibie_list, dict):
                                        # 直接按定义顺序保留value
                                        jiaobiao_binary_shibie_list = list(jiaobiao_binary_shibie_list.values())
                                    # jiaobiao_binary_shibie_list 元素都转换为数字，如果不是数字，则转换为0
                                    jiaobiao_binary_shibie_list = [int(item) if str(item).isdigit() else 0 for item in jiaobiao_binary_shibie_list]
                                if 识别recognized_text方法序号 == 2:
                                    import json
                                    # 去除多余的字符
                                    json_str = recognized_text.strip().replace('```json', '').replace('```', '').strip()
                                    # 解析JSON
                                    parsed_data = json.loads(json_str)  # 解析为字典
                                    # 检查是否存在 'numbers' 键
                                    if parsed_data:  # 检查字典是否非空
                                        jiaobiao_binary_shibie_list = list(parsed_data.values())[0]  # 提取所有值并转换为列表
                                        # 如果 parsed_data 中有两个键，parsed_data.values() 会提取这两个键对应的值，并按照字典中键的顺序将它们放入列表中
                                        # jiaobiao_binary_shibie_list = [[210, 101, 208], [104, 306]]
                                    else:
                                        jiaobiao_binary_shibie_list = []  # 如果字典为空，返回空列表
                                    # 将元素转换为数字，如果不是数字，则转换为0
                                    jiaobiao_binary_shibie_list = [int(item) if str(item).isdigit() else 0 for item in jiaobiao_binary_shibie_list]
                                    print("初步角标图像识别结果:", jiaobiao_binary_shibie_list)

                                # 遍历 jiaobiao_binary_shibie_list ，检查是否为3位数字且首位是1/2/3，否则打印异常
                                
                                for idx, item in enumerate(jiaobiao_binary_shibie_list):
                                    item_str = str(item)  # 转换为字符串方便检查
                                    if len(item_str) == 3 and item_str[0] in ['1', '2', '3'] and item_str.isdigit():
                                        jiaobiao_binary_shibie_true_false_list.append(True)
                                    else:
                                        jiaobiao_binary_shibie_true_false_list.append(False)
                                        print(f"异常：索引 {idx} 的元素 '{item}' 不符合要求（需为3位数字且首位为1/2/3）")
                                # print("再次角标图像识别结果:", jiaobiao_binary_shibie_list)
                            except Exception as e:
                                print(f"解析JSON失败: {str(e)}")
                                jiaobiao_binary_shibie_list = []
                            
                            # 判断 jiaobiao_binary_shibie_list 个数是否等于 jiaobiao_binary_list 个数
                            if len(jiaobiao_binary_shibie_list) < len(jiaobiao_binary_list):
                                # 计算需要补充的元素数量
                                num_to_add = len(jiaobiao_binary_list) - len(jiaobiao_binary_shibie_list)
                                # 补充0到识别结果列表
                                jiaobiao_binary_shibie_list.extend([0] * num_to_add)
                                # 补充False到验证列表
                                jiaobiao_binary_shibie_true_false_list.extend([False] * num_to_add)
                                # 打印调试信息
                                print(f"识别结果个数和角标图像个数不一致，给识别结果补充了 {num_to_add} 个元素，现在 jiaobiao_binary_shibie_list 有 {len(jiaobiao_binary_shibie_list)} 个元素")
                            elif len(jiaobiao_binary_shibie_list) > len(jiaobiao_binary_list):
                                # 计算需要删除的元素数量
                                num_to_remove = len(jiaobiao_binary_shibie_list) - len(jiaobiao_binary_list)
                                # 删除多余部分
                                jiaobiao_binary_shibie_list = jiaobiao_binary_shibie_list[:len(jiaobiao_binary_list)]
                                jiaobiao_binary_shibie_true_false_list = jiaobiao_binary_shibie_true_false_list[:len(jiaobiao_binary_list)]
                                # 打印调试信息
                                print(f"删除了 {num_to_remove} 个元素，现在 jiaobiao_binary_shibie_list 有 {len(jiaobiao_binary_shibie_list)} 个元素")
                            print("最终角标图像识别结果:", jiaobiao_binary_shibie_list)
                            # 如果  # 判断 jiaobiao_binary_shibie_list 个数是否等于 jiaobiao_binary_list 个数，不相等则 jiaobiao_binary_shibie_true_false_list 全部设置为False
                            if len(jiaobiao_binary_shibie_list) != len(jiaobiao_binary_list):
                                jiaobiao_binary_shibie_true_false_list = [False] * len(jiaobiao_binary_list)
                            # 如果 jiaobiao_binary_shibie_true_false_list 全是 True
                            if all(jiaobiao_binary_shibie_true_false_list):
                                print("所有角标图像识别结果都符合要求！")
                                break
                        #
                        if 识别方法选择 == 2:
                            # 识别方法2：调用paddleocr
                            角标图像个数 = len(jiaobiao_binary_list)
                            # jiaobiao_binary_list 复制一份到 jiaobiao_binary_list_copy
                            jiaobiao_binary_list_copy = jiaobiao_binary_list.copy()
                            for 识别次数 in range(2):
                                y_offset = 0
                                for img in jiaobiao_binary_list:
                                    h, w = img.shape
                                    vertical_concatenated_img[y_offset:y_offset+h, :w] = img
                                    y_offset += h
                                # 把纵向拼接的图赋值给 concatenated_img，竖着识别率似乎更高
                                concatenated_img = vertical_concatenated_img
                                # concatenated_img = cv2.cvtColor(concatenated_img, cv2.COLOR_GRAY2RGB)

                                # 把 角标图像个数 分成多组，每组最多9个，每组分别识别，比如 10就分成 9 1，22就分成9 9 3
                                result_合并 = [] # 把每组的识别结果合并到一起
                                # 分组
                                分组_list = []
                                角标图像个数 = len(jiaobiao_binary_list)
                                每组最大数量 = 9
                                # 计算分组
                                for i in range(角标图像个数//每组最大数量): 分组_list.append(9)
                                if 角标图像个数 % 每组最大数量 != 0: 分组_list.append(角标图像个数 % 每组最大数量)
                                # print(分组_list)  # 输出: [9, 9, 3]
                                
                                # 初始化 im_show_list
                                im_show_list = []
                                for idx, 分组 in enumerate(分组_list):
                                    try:
                                        # 保存 concatenated_img[idx*900:idx*900+分组*900,:] 到 concatenated_path.replace('角标裁剪', '角标裁剪'+str(idx))
                                        temp_img = concatenated_img[idx*900:idx*900+分组*100,:]
                                        temp_path = concatenated_path.replace('角标裁剪', '角标裁剪_'+str(idx))
                                        # cv2.imwrite(temp_path, temp_img)
                                        cv2.imencode('.png', temp_img)[1].tofile(temp_path)

                                        result = ocr.ocr(temp_img, cls=False)
                                        result_合并.append(result)

                                        # draw result
                                        from PIL import Image
                                        result = result[0] # 取出第一个结果
                                        image = Image.open(temp_path).convert('RGB')
                                        # 删除图片 temp_path
                                        os.remove(temp_path)
                                        boxes = [line[0] for line in result]
                                        txts = [line[1][0] for line in result]
                                        scores = [line[1][1] for line in result]
                                        im_show = draw_ocr(image, boxes, txts, scores)
                                        im_show = Image.fromarray(im_show)
                                        concatenated_result_path = concatenated_path.replace('角标裁剪', '角标识别')
                                        # im_show.save(concatenated_result_path)
                                        
                                        # 把 im_show 保存到 im_show_list，最后垂直拼接不要缩放保存到concatenated_result_path
                                        # 将当前分组的识别结果添加到列表中
                                        im_show_list.append(im_show)
                                        pass
                                    except:
                                        print('分组识别失败！')
                                # 垂直拼接所有识别结果
                                if not im_show_list:
                                    print('所有分组识别都失败，跳过识别结果图像生成')
                                    break
                                # 确保所有 im_show 的宽度一致
                                target_width = max(img.width  for img in im_show_list)  # 取最大宽度
                                total_height = sum(img.height for img in im_show_list)  # 计算总高度

                                # 创建白色背景画布
                                final_im_show = np.ones((total_height, target_width, 3), dtype=np.uint8) * 255

                                # 从上往下依次贴图
                                y_offset = 0
                                for img in im_show_list:  # img 是 'Image' object
                                    img_np = np.array(img)  # 将 Image 对象转换为 numpy 数组
                                    h, w = img_np.shape[:2]  # 现在可以正常获取高度和宽度
                                    final_im_show[y_offset:y_offset+h, :w] = img_np
                                    y_offset += h

                                # 保存最终结果
                                concatenated_result_path = concatenated_path.replace('角标裁剪', '角标识别_'+str(识别次数))
                                Image.fromarray(final_im_show).save(concatenated_result_path) # final_im_show

                                try:
                                    # 遍历 result_合并
                                    for 分组_idx in range(len(result_合并)):
                                        result_tmp = result_合并[分组_idx]
                                        try:#吹每组的识别结果
                                            for idx in range(len(result_tmp)):
                                                res = result_tmp[idx]
                                                jiaobiao_binary_index = int(0)
                                                for line in res:
                                                    # print(line)
                                                    左上角_y = line[0][0][1] + 分组_idx * 900 # 注意加上前面的分组像素高度
                                                    右下角_y = line[0][2][1] + 分组_idx * 900 # 注意加上前面的分组像素高度
                                                    中心点_y = (左上角_y + 右下角_y) / 2
                                                    item_str = str(line[1][0])
                                                    jiaobiao_binary_index = int(中心点_y // 100) # 5//100 = 0 105//100 = 1

                                                    # 遍历 item_str 中的字符，处理G+编号或编号+G的情况
                                                    item_str_原始 = item_str
                                                    
                                                    # 检查是否包含G和数字的组合
                                                    has_g = 'G' in item_str or 'g' in item_str
                                                    has_digits = any(char.isdigit() for char in item_str)
                                                    
                                                    # 如果包含G和数字，尝试提取编号
                                                    if has_g and has_digits:
                                                        # 提取所有数字
                                                        digits = ''.join([char for char in item_str if char.isdigit()])
                                                        if len(digits) >= 3:
                                                            # 取前3位数字，确保首位是1/2/3
                                                            if digits[0] in ['1', '2', '3']:
                                                                extracted_number = digits[:3]
                                                                jiaobiao_binary_shibie_list[jiaobiao_binary_index] = int(extracted_number)
                                                                jiaobiao_binary_shibie_true_false_list[jiaobiao_binary_index] = True
                                                                jiaobiao_binary_shibie_备注_list[jiaobiao_binary_index] = "G"
                                                                print(f"G+编号识别成功: '{item_str_原始}' -> 编号: {extracted_number}, 备注: G")
                                                                continue  # 跳过后续处理
                                                            else:
                                                                # 如果首位不是1/2/3，尝试构造
                                                                if len(digits) >= 2:
                                                                    extracted_number = '1' + digits[:2]
                                                                    jiaobiao_binary_shibie_list[jiaobiao_binary_index] = int(extracted_number)
                                                                    jiaobiao_binary_shibie_true_false_list[jiaobiao_binary_index] = True
                                                                    jiaobiao_binary_shibie_备注_list[jiaobiao_binary_index] = "G"
                                                                    print(f"G+编号识别成功: '{item_str_原始}' -> 编号: {extracted_number}, 备注: G")
                                                                    continue  # 跳过后续处理
                                                        elif len(digits) >= 2:
                                                            # 数字不足3位，用1开头填充
                                                            extracted_number = '1' + digits[:2]
                                                            jiaobiao_binary_shibie_list[jiaobiao_binary_index] = int(extracted_number)
                                                            jiaobiao_binary_shibie_true_false_list[jiaobiao_binary_index] = True
                                                            jiaobiao_binary_shibie_备注_list[jiaobiao_binary_index] = "G"
                                                            print(f"G+编号识别成功: '{item_str_原始}' -> 编号: {extracted_number}, 备注: G")
                                                            continue  # 跳过后续处理
                                                    
                                                    # 原有的字符替换逻辑
                                                    item_str = item_str.replace('/', '7') # 31/ -> 317
                                                    item_str = item_str.replace('C', '0') # 3C1 -> 301
                                                    item_str = item_str.replace(' ', '') # 2 1 4 -> 214
                                                    # item_str = ''.join([char for char in item_str if char.isdigit()]) # 
                                                    
                                                    
                                                    if len(item_str) == 3 and item_str[0] in ['1', '2', '3'] and item_str.isdigit():
                                                        jiaobiao_binary_shibie_list[jiaobiao_binary_index] = int(item_str)
                                                        jiaobiao_binary_shibie_true_false_list[jiaobiao_binary_index] = True
                                                        print(f"标准识别成功: '{item_str_原始}' -> '{item_str}'")
                                                    else:
                                                        jiaobiao_binary_shibie_list[jiaobiao_binary_index] = 0 # 0表示无效客户编号
                                                        print(f"识别失败: '{item_str_原始}' -> '{item_str}' (无效格式)")
                                        except:
                                            print('识别失败！')
                                except:
                                    print('识别失败！')
                                # 如果 jiaobiao_binary_shibie_true_false_list 全是 True 则 break
                                if all(jiaobiao_binary_shibie_true_false_list):
                                    print("所有角标图像识别结果都符合要求！")
                                    break
                                for idx, item in enumerate(jiaobiao_binary_shibie_true_false_list):
                                    if item == False:
                                        # 把 jiaobiao_binary_list 中的图像元素顺时针旋转180度
                                        jiaobiao_binary_list[idx] = cv2.rotate(jiaobiao_binary_list[idx], cv2.ROTATE_180) 
                            print(jiaobiao_binary_shibie_list)
                            print(jiaobiao_binary_shibie_true_false_list)
                            jiaobiao_binary_list = jiaobiao_binary_list_copy.copy()
                            # 拼接角标识别图片
                            concatenated_result_path_0 = concatenated_path.replace('角标裁剪', '角标识别_'+str(0))
                            concatenated_result_path_1 = concatenated_path.replace('角标裁剪', '角标识别_'+str(1))
                            concatenated_result_path = concatenated_path.replace('角标裁剪', '角标识别')
                            # 如果不存在 concatenated_result_path_1 则把 名为concatenated_result_path_0的图像文件 重命名为 concatenated_result_path
                            if not os.path.exists(concatenated_result_path_1):
                                try:
                                    os.remove(os.remove(concatenated_result_path))
                                except:
                                    pass
                                if os.path.exists(concatenated_result_path_0):
                                    os.rename(concatenated_result_path_0, concatenated_result_path)
                            else:
                                # concatenated_result_path_0 在左，concatenated_result_path_1在右拼接得到 concatenated_result_path，不要缩放两个图片，把他们靠在一起贴到一个大的白色背景图像上
                                # 读取两个图片
                                with open(concatenated_result_path_0, 'rb') as f:
                                    img_buffer = np.frombuffer(f.read(), dtype=np.uint8)
                                img0 = cv2.imdecode(img_buffer, cv2.IMREAD_COLOR)
                                with open(concatenated_result_path_1, 'rb') as f:
                                    img_buffer = np.frombuffer(f.read(), dtype=np.uint8)
                                img1 = cv2.imdecode(img_buffer, cv2.IMREAD_COLOR)
                                
                                # 获取图片尺寸
                                h0, w0, _ = img0.shape
                                h1, w1, _ = img1.shape
                                
                                # 计算拼接后的画布大小
                                max_height = max(h0, h1)
                                total_width = w0 + w1
                                
                                # 创建白色背景画布
                                combined_img = np.ones((max_height, total_width, 3), dtype=np.uint8) * 255
                                
                                # 将两个图片放置到画布上
                                combined_img[0:h0, 0:w0] = img0
                                combined_img[0:h1, w0:w0+w1] = img1
                                
                                # 保存拼接后的图片
                                cv2.imencode('.png', combined_img)[1].tofile(concatenated_result_path)
                                
                                # 删除原来的两个图片
                                os.remove(concatenated_result_path_0)
                                os.remove(concatenated_result_path_1)


                    # 把pdf每页的角标识别结果保存到excel表格中（保存到输出目录）
                    识别结果文件名 = os.path.basename(image_path).replace('.png', '.xlsx')
                    识别结果excel_path = os.path.join(output_dir, 识别结果文件名)
                    # 字段：PDF文件名、页码、黑框图像、角标图像、客户编号（角标图像识别结果）
                    # PDF文件名 = os.path.basename(image_path.split('_page')[0]+'.pdf')
                    PDF文件名 = image_path.replace('.png', '')
                    # 页码 = image_path.split('_')[1].split('page')[-1].split('.')[0]
                    页码 = 1
                    # 黑框图像对应 juxing_binary_list 中的图像元素
                    # 角标图像对应 jiaobiao_binary_list 中的图像元素
                    # 客户名对应 jiaobiao_binary_shibie_list 中的元素
                    # 把图像按原始数据保存到excel表格中，方便后续python提取出原始图像数据
                    # 单元格里的图像要撑开到统一的尺寸，便于excel打开查看
                    # 创建Excel文件
                    wb = Workbook()
                    ws = wb.active

                    # 添加表头
                    ws.append(['PDF文件名', '图案编号', '黑框图像', '长\n（mm）', '宽\n（mm）', '面积\n（mm²）', '角标图像', '客户编号', '识别状态', '备注', '客户名称', '单价', '总额'])
                    
                    # 添加筛选按钮（新增代码）
                    ws.auto_filter.ref = ws.dimensions  # 自动识别数据范围                    

                    # 添加数据行
                    from openpyxl.styles import PatternFill
                    # 定义淡红色填充
                    light_red_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
                    
                    pdf第几次放入 = 0
                    # 清空 中间图像输出文件夹 下面的所有文件夹和文件
                    import shutil
                    if os.path.exists(中间图像输出文件夹): 
                        # shutil.rmtree(中间图像输出文件夹)
                        pdf第几次放入 = 2
                        # 删除 中间图像输出文件夹_没制作 文件夹里面的png
                        没制作文件夹 = os.path.join(os.path.dirname(中间图像输出文件夹), "中间图像输出文件夹_没制作")
                        if os.path.exists(没制作文件夹):
                            for file in os.listdir(没制作文件夹):
                                if file.endswith(".png"):
                                    os.remove(os.path.join(没制作文件夹, file))
                    else:
                        pdf第几次放入 = 1
                        os.makedirs(中间图像输出文件夹, exist_ok=True)
                    # 遍历 中间图像输出文件夹 里面的所有png文件名
                    # 初始化空列表
                    png_list = []
                    png_abspath_list = []
                    # 递归遍历文件夹
                    for root, dirs, files in os.walk(中间图像输出文件夹):
                        for file in files:
                            if file.endswith(".png"):
                                # 将文件名添加到列表中
                                png_list.append(file)
                                # 获取文件的绝对路径
                                png_abspath = os.path.join(root, file)
                                # 将绝对路径添加到列表中
                                png_abspath_list.append(png_abspath)
                    # 打印结果
                    # print(png_list)
                    # 创建子文件夹
                    subfolders = ["temp", "没制作", "制作中", "制作好", "质检不合格"]
                    for subfolder in subfolders:
                        subfolder_path = os.path.join(中间图像输出文件夹, subfolder)
                        os.makedirs(subfolder_path, exist_ok=True)
                        # print(f"子文件夹 {subfolder_path} 已创建")
                    中间图像输出文件夹_temp = os.path.join(中间图像输出文件夹, "temp")
                    中间图像输出文件夹_没制作 = os.path.join(中间图像输出文件夹, "没制作")
                    中间图像输出文件夹_制作中 = os.path.join(中间图像输出文件夹, "制作中")
                    中间图像输出文件夹_制作好 = os.path.join(中间图像输出文件夹, "制作好")
                    中间图像输出文件夹_质检不合格 = os.path.join(中间图像输出文件夹, "质检不合格")

                    # 为每个元素添加索引（相同元素的索引累加）
                    jiaobiao_binary_shibie_list_编号_list = []
                    # 创建计数字典，记录每个元素出现的次数
                    元素计数 = {}
                    
                    for 元素 in jiaobiao_binary_shibie_list:
                        if 元素 not in 元素计数:
                            元素计数[元素] = 1
                        else:
                            元素计数[元素] += 1
                        # 拼接格式：元素_索引
                        编号元素 = f"{元素}_{元素计数[元素]}"
                        jiaobiao_binary_shibie_list_编号_list.append(编号元素)
                        
                    for i in range(len(juxing_binary_list)):
                        # 添加文本数据（使用os.path.basename获取纯文件名）
                        # PDF文件名 = os.path.basename(image_path.split('_page')[0])
                        PDF文件名 = os.path.basename(image_path.replace('.png', ''))
                        # 图案编号 = PDF文件名+'_'+str(i+1)+'_'+str(jiaobiao_binary_shibie_list[i])
                        图案编号 = f"{PDF文件名}_{jiaobiao_binary_shibie_list_编号_list[i]}"
                        
                        # 获取当前日期（格式：20250525）
                        current_date = datetime.datetime.now().strftime('%Y%m%d')
                        
                        # 查询客户名称和单价
                        customer_name, price = query_customer_name_and_price(jiaobiao_binary_shibie_list[i], current_date)
                        
                        # 计算总额：长(cm) × 宽(cm) × 单价
                        # mm转cm需要除以10，所以 长mm × 宽mm × 单价 / 100
                        total_amount = ""
                        if price and juxing_length_mm_list[i] and juxing_width_mm_list[i]:
                            try:
                                length_cm = juxing_length_mm_list[i] / 10
                                width_cm = juxing_width_mm_list[i] / 10
                                total_amount = round(length_cm * width_cm * float(price), 2)
                            except:
                                total_amount = ""
                        
                        ws.append([
                            PDF文件名, 
                            图案编号,# 图案编号
                            None, 
                            juxing_length_mm_list[i],  # D列：长(mm)
                            juxing_width_mm_list[i],   # E列：宽(mm)
                            mianji_mm_list[i],         # F列：面积(mm²)
                            None, 
                            jiaobiao_binary_shibie_list[i],  # H列：客户编号
                            "正常" if jiaobiao_binary_shibie_true_false_list[i] else "异常",  # I列：识别状态
                            jiaobiao_binary_shibie_备注_list[i],  # J列：备注
                            customer_name if customer_name else "",  # K列：客户名称
                            price if price else "",  # L列：单价
                            total_amount  # M列：总额
                        ])

                        # 如果识别结果异常，设置整行背景色为淡红色
                        if not jiaobiao_binary_shibie_true_false_list[i]:
                            for col in range(1, 14):  # 从A到M列（现在13列）
                                ws.cell(row=i+2, column=col).fill = light_red_fill
                        
                        # 保存黑框图像为临时文件
                        juxing_temp_filename = f"{图案编号}.png"
                        juxing_temp_path = ""
                        if juxing_temp_filename not in png_list:
                            pdf第几次放入 = 1
                            if pdf第几次放入 == 1:
                                juxing_temp_path = os.path.join(中间图像输出文件夹_没制作, juxing_temp_filename)
                            if pdf第几次放入 == 2:
                                juxing_temp_path = os.path.join(中间图像输出文件夹_制作好, juxing_temp_filename)
                            cv2.imencode('.png', juxing_binary_list[i])[1].tofile(juxing_temp_path)
                        else:
                            # print(f"{juxing_temp_filename}已存在，跳过")
                            juxing_temp_path = next((path for path in png_abspath_list if path.endswith(juxing_temp_filename)), None)

                        if (
                            jiaobiao_binary_shibie_true_false_list[i]
                            and jiaobiao_binary_shibie_list[i]
                        ):
                            original_pdf = os.path.basename(image_path).split('_page')[0].replace('.png', '') + ".pdf"
                            if juxing_temp_path:
                                try:
                                    relative_path = os.path.relpath(juxing_temp_path, output_dir)
                                except ValueError:
                                    relative_path = os.path.basename(juxing_temp_path)
                                black_box_path = relative_path.replace('\\', '/')
                            else:
                                black_box_path = ""
                            upsert_order_status(
                                 serial_number=图案编号,
                                 identifier=str(jiaobiao_binary_shibie_list[i]),
                                 status="未制作",
                                 pdf_name=os.path.basename(original_pdf),
                                 black_box_path=black_box_path
                             )
                        
                        # 添加黑框图像（修正列位置为C）
                        img = ExcelImage(juxing_temp_path)
                        img.width = 100
                        img.height = 100
                        ws.add_image(img, f'C{i+2}')  # 原为D列，修正为C列
                        
                        # 保存角标图像为临时文件
                        jiaobiao_temp_path = f"temp_jiaobiao_{i}.png"
                        jiaobiao_temp_path = os.path.join(中间图像输出文件夹_temp, jiaobiao_temp_path)
                        cv2.imwrite(jiaobiao_temp_path, jiaobiao_binary_list[i])
                        
                        # 添加角标图像（修正列位置为G）
                        img = ExcelImage(jiaobiao_temp_path)
                        img.width = 100
                        img.height = 100
                        ws.add_image(img, f'G{i+2}')  # 原为D列，修正为G列
                    
                    # 设置所有单元格对齐方式
                    for row in ws.iter_rows(min_row=1, max_row=len(juxing_binary_list)+1, max_col=13):  # 增加列数到13列
                        for cell in row:
                            cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
                    
                    # 调整列宽
                    ws.column_dimensions['A'].width = 30  # 原为20，加宽以显示长文件名
                    ws.column_dimensions['B'].width = 10
                    ws.column_dimensions['C'].width = 15
                    ws.column_dimensions['D'].width = 15
                    ws.column_dimensions['E'].width = 15
                    ws.column_dimensions['F'].width = 15
                    ws.column_dimensions['G'].width = 20
                    ws.column_dimensions['I'].width = 15  # 识别状态列宽
                    ws.column_dimensions['K'].width = 20  # 客户名称列宽
                    ws.column_dimensions['L'].width = 15  # 单价列宽
                    ws.column_dimensions['M'].width = 15  # 总额列宽

                    # 调整行高，确保图像能够显示
                    for i in range(2, len(jiaobiao_binary_list) + 2):  # 使用角标列表长度，从第二行开始
                        ws.row_dimensions[i].height = 75  # 100像素对应75磅（1像素≈0.75磅）

                    # 为识别状态列添加筛选条件
                    ws.auto_filter.add_filter_column(8, ["正常", "异常"])  # 索引8表示第9列（I列）

                    # 保存Excel文件
                    wb.save(识别结果excel_path)
                    print(f"识别结果已保存到: {识别结果excel_path}")
                    
                    # 立即合并到merge.xlsx
                    merge_file = os.path.join(output_dir, 'merge.xlsx')
                    merge_single_excel_to_merge_file(识别结果excel_path, merge_file, output_dir)
                    print()  # 空行分隔
                    
            except Exception as e:
                print(f"处理图像 {png_path} 时出错: {str(e)}\n")  # 保留原有错误信息

    print(f"PNG目录处理完成，共处理了 {processed_count} 个PNG文件！")
    
    # 打印合并文件的最终位置
    merge_file = os.path.join(output_dir, 'merge.xlsx')
    if os.path.exists(merge_file):
        print(f"📊 所有Excel文件已合并到: {merge_file}")
    
    return processed_count

def run_script2():
    # 使用配置文件中的PDF绝对路径
    input_dir, output_dir = get_pdf_absolute_path()
    print(f"run_script2 输入目录: {input_dir}")
    print(f"run_script2 输出目录: {output_dir}")
    
    file_server_root = output_dir or input_dir
    start_file_server(file_server_root)
     
    # 处理PDF基础目录
    process_png_directory(input_dir, output_dir)
    
    # 日期 20250319 格式
    日期 = datetime.datetime.now().strftime('%Y%m%d')
    input_date_dir = os.path.join(input_dir, 日期)  # 输入的日期目录
    output_date_dir = os.path.join(output_dir, 日期)  # 输出的日期目录
    
    print(f"run_script2 输入日期目录: {input_date_dir}")
    print(f"run_script2 输出日期目录: {output_date_dir}")
    
    # 确保输出日期目录存在
    if not os.path.exists(output_date_dir):
        print(f"输出日期目录不存在，正在创建: {output_date_dir}")
        os.makedirs(output_date_dir, exist_ok=True)
    
    # 如果输入目录存在，则处理
    if os.path.exists(input_date_dir):
        process_png_directory(input_date_dir, output_date_dir)
    else:
        print(f"输入日期目录不存在: {input_date_dir}")

def close_wps_main():
    """
    关闭所有WPS Office主进程
    """
    found = False
    for proc in psutil.process_iter(['pid', 'name']):
        try:
            if proc.info['name'].lower() in ['wpsoffice.exe', 'wps.exe']:
                found = True
                print(f"找到WPS主进程 PID: {proc.info['pid']}, 名称: {proc.info['name']}")
                print(f"尝试终止WPS...")
                
                # 先尝试温和的终止方法
                proc.terminate()
                # 等待进程结束 (最多等待3秒)
                try:
                    proc.wait(timeout=3)
                except psutil.TimeoutExpired:
                    # 如果超时，强制终止进程
                    print(f"WPS没有正常终止，尝试强制终止")
                    proc.kill()
                
                print(f"已关闭WPS进程 {proc.info['pid']}")
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess) as e:
            print(f"访问进程时出错: {e}")
    
    if not found:
        # print("未找到WPS Office主进程")
        pass

def merge_single_excel_to_merge_file(excel_file, merge_file, output_dir):
    """
    将单个Excel文件追加合并到merge.xlsx文件中（包括图片）
    
    参数:
        excel_file: 要合并的单个Excel文件路径
        merge_file: 合并后的目标文件路径（merge.xlsx）
        output_dir: 输出目录
    """
    try:
        # 如果merge.xlsx不存在，创建新文件
        if not os.path.exists(merge_file):
            print(f"创建新的合并文件: {merge_file}")
            merged_wb = Workbook()
            merged_ws = merged_wb.active
            merged_ws.title = '总表'
            
            # 从源文件读取数据（包括标题行）
            source_wb = load_workbook(excel_file)
            source_ws = source_wb.active
            
            # 复制所有行（包括标题）
            for row_idx, row in enumerate(source_ws.iter_rows(), start=1):
                for cell in row:
                    new_cell = merged_ws.cell(row=row_idx, column=cell.col_idx, value=cell.value)
                    if cell.has_style:
                        new_cell.font = cell.font.copy()
                        new_cell.border = cell.border.copy()
                        new_cell.fill = cell.fill.copy()
                        new_cell.alignment = cell.alignment.copy()
            
            # 复制列宽
            for col_letter in source_ws.column_dimensions:
                if col_letter in source_ws.column_dimensions:
                    merged_ws.column_dimensions[col_letter].width = source_ws.column_dimensions[col_letter].width
            
            # 复制行高
            for row_num in source_ws.row_dimensions:
                if row_num in source_ws.row_dimensions:
                    merged_ws.row_dimensions[row_num].height = source_ws.row_dimensions[row_num].height
            
             # 复制图片（第一个文件，行号不变）
            if hasattr(source_ws, '_images') and source_ws._images:
                for img in source_ws._images:
                    # 创建新图片对象
                    new_img = ExcelImage(img.ref)
                    # 设置固定的宽高（100x100像素）
                    new_img.width = 100
                    new_img.height = 100
                    # 保持原始位置
                    new_img.anchor = img.anchor
                    merged_ws.add_image(new_img)
            
            source_wb.close()
            merged_wb.save(merge_file)
            print(f"✅ 已创建合并文件并添加第一个文件: {os.path.basename(excel_file)}")
        else:
            # merge.xlsx已存在，追加数据
            merged_wb = load_workbook(merge_file)
            merged_ws = merged_wb.active
            
            # 获取当前最后一行
            last_row = merged_ws.max_row
            
            # 从源文件读取数据（跳过标题行）
            source_wb = load_workbook(excel_file)
            source_ws = source_wb.active
            
            # 计算行偏移量（源文件第2行对应目标文件的last_row+1行）
            row_offset = last_row - 1  # 源文件的第2行 -> 目标文件的 last_row+1 行
            
            # 复制数据行（从第2行开始，跳过标题）
            for row_idx, row in enumerate(source_ws.iter_rows(min_row=2), start=2):
                target_row = last_row + row_idx - 1
                for cell in row:
                    new_cell = merged_ws.cell(row=target_row, column=cell.col_idx, value=cell.value)
                    if cell.has_style:
                        new_cell.font = cell.font.copy()
                        new_cell.border = cell.border.copy()
                        new_cell.fill = cell.fill.copy()
                        new_cell.alignment = cell.alignment.copy()
                
                # 复制行高
                if row_idx in source_ws.row_dimensions:
                    merged_ws.row_dimensions[target_row].height = source_ws.row_dimensions[row_idx].height
            
             # 复制图片（需要调整行号）
            if hasattr(source_ws, '_images') and source_ws._images:
                for img in source_ws._images:
                    # 创建新图片对象
                    new_img = ExcelImage(img.ref)
                    # 设置固定的宽高（100x100像素）
                    new_img.width = 100
                    new_img.height = 100
                    
                    # 解析原始锚点位置（例如 'C2'）
                    original_anchor = img.anchor
                    if hasattr(original_anchor, '_from'):
                        # 获取原始行号
                        original_row = original_anchor._from.row
                        # 如果图片在数据行（第2行及以后），需要调整行号
                        if original_row >= 1:  # Excel的行号从0开始，第2行是索引1
                            new_row = original_row + row_offset
                            # 获取列标识
                            col_idx = original_anchor._from.col
                            # 创建新的锚点位置
                            from openpyxl.utils import get_column_letter
                            col_letter = get_column_letter(col_idx + 1)  # openpyxl列索引从0开始
                            new_anchor = f'{col_letter}{new_row + 1}'  # Excel行号从1开始
                            new_img.anchor = new_anchor
                        else:
                            new_img.anchor = original_anchor
                    else:
                        # 如果是字符串锚点（如 'C2'）
                        import re
                        match = re.match(r'([A-Z]+)(\d+)', str(original_anchor))
                        if match:
                            col_letter = match.group(1)
                            original_row_num = int(match.group(2))
                            # 如果图片在数据行（第2行及以后），需要调整行号
                            if original_row_num >= 2:
                                new_row_num = original_row_num + row_offset
                                new_anchor = f'{col_letter}{new_row_num}'
                                new_img.anchor = new_anchor
                            else:
                                new_img.anchor = original_anchor
                        else:
                            new_img.anchor = original_anchor
                    
                    merged_ws.add_image(new_img)
            
            source_wb.close()
            merged_wb.save(merge_file)
            print(f"✅ 已追加合并文件: {os.path.basename(excel_file)} -> merge.xlsx (当前总行数: {merged_ws.max_row})")
    
    except Exception as e:
        print(f"⚠️ 合并Excel文件时出错: {str(e)}")
        import traceback
        traceback.print_exc()

def merge_px_excel_files(base_dir=None, pdf_dirname='pdf', sheet_name='总表'):
    """
    合并指定目录及其子目录中以P/p开头的Excel文件
    
    参数:
    base_dir: 基础目录，默认为当前脚本所在目录
    pdf_dirname: pdf目录名称，默认为'pdf'
    sheet_name: 合并后的工作表名称，默认为'总表'
    
    返回:
    处理的文件夹列表和每个文件夹中处理的文件数
    """
    # 获取基础目录
    if base_dir is None:
        if getattr(sys, 'frozen', False):
            # 如果是打包后的exe
            base_dir = os.path.dirname(sys.executable)
        else:
            # 如果是脚本运行
            base_dir = os.path.dirname(os.path.abspath(__file__))
    
    # 设置pdf目录 - 使用绝对路径配置
    if pdf_dirname == 'pdf':  # 如果是默认的pdf目录名，使用配置文件路径
        input_dir, output_dir = get_pdf_absolute_path()
        pdf_dir = output_dir  # 使用输出目录查找生成的Excel文件
    else:
        pdf_dir = os.path.join(base_dir, pdf_dirname)
    
    # 获取需要处理的所有文件夹（pdf_dir本身和其直接子文件夹）
    folders_to_process = [base_dir]  # 添加基础目录
    if os.path.exists(pdf_dir):
        folders_to_process.append(pdf_dir)  # 添加pdf目录
        # 添加pdf目录下的所有子文件夹
        for item in os.listdir(pdf_dir):
            item_path = os.path.join(pdf_dir, item)
            if os.path.isdir(item_path):
                folders_to_process.append(item_path)
    
    # print(f"要处理的文件夹: {folders_to_process}")
    
    # 用于存储处理结果的字典
    results = {}
    
    # 处理每个文件夹
    for folder in folders_to_process:
        if 'pdf' not in folder:
            continue
        # 获取所有以 p/P 开头的 xlsx 文件
        files = glob.glob(os.path.join(folder, '[pP]*.xlsx'))
        
        # 如果没有找到文件，跳过此文件夹
        if not files:
            # print(f"在 {folder} 中没有找到P/p开头的xlsx文件，跳过处理。")
            results[folder] = 0
            continue
        
        # 按文件名中的数字排序（提取 P/p 后 - 前的数字）
        files.sort(key=lambda x: int(re.search(r'[pP](\d+)', os.path.basename(x)).group(1)))
        
        # 打印找到的文件
        # print(f"\n正在处理文件夹 {folder}:")
        # print(f"找到的文件: {files}")
    
        try:
            # 创建一个新的工作簿用于保存合并后的数据
            merged_wb = Workbook()
            merged_ws = merged_wb.active
            # 将工作表命名为指定名称
            merged_ws.title = sheet_name
    
            # 初始化行计数器
            current_row = 1
            
            # 遍历所有文件并合并
            for file_index, file in enumerate(files):
                wb = load_workbook(file)
                ws = wb.active
                
                # 确定开始行的索引
                start_row = 1
                # 如果不是第一个文件，则跳过第一行标题
                if file_index > 0:
                    start_row = 2
                
                # 复制单元格内容和样式
                for row_idx, row in enumerate(ws.iter_rows(min_row=start_row), start=start_row):
                    for cell in row:
                        # 计算目标行索引
                        target_row = current_row + row_idx - start_row
                        new_cell = merged_ws.cell(row=target_row, column=cell.col_idx, value=cell.value)
                        if cell.has_style:
                            new_cell.font = cell.font.copy()
                            new_cell.border = cell.border.copy()
                            new_cell.fill = cell.fill.copy()
                            new_cell.number_format = cell.number_format
                            new_cell.protection = cell.protection.copy()
                            new_cell.alignment = cell.alignment.copy()
                
                # 复制行高和列宽
                for idx, row in enumerate(ws.iter_rows(min_row=start_row), start=start_row):
                    target_row = current_row + idx - start_row
                    merged_ws.row_dimensions[target_row].height = ws.row_dimensions[row[0].row].height
                
                # 仅对第一个文件设置列宽
                if file_index == 0:
                    for idx, col in enumerate(ws.iter_cols(), 1):
                        merged_ws.column_dimensions[get_column_letter(idx)].width = ws.column_dimensions[col[0].column_letter].width
    
                # 复制图片
                for image in ws._images:
                    try:
                        # 获取原始图片的位置信息
                        original_from_row = image.anchor._from.row
                        
                        # 计算新的from行位置
                        if file_index > 0:
                            # 非第一个文件需要考虑跳过标题行
                            # 向上调整一行来修复图片下移问题
                            new_from_row = current_row + original_from_row - (start_row - 1) - 1
                        else:
                            # 第一个文件直接加偏移量，同样向上调整一行
                            new_from_row = current_row + original_from_row - 1
                        
                        # 根据锚点类型调整图片位置
                        # 检查是否是TwoCellAnchor类型(有_to属性)还是OneCellAnchor类型
                        is_two_cell_anchor = hasattr(image.anchor, '_to')
                        
                        # 设置新的起始行
                        image.anchor._from.row = new_from_row
                        
                        # 如果是双单元格锚点，则也需要调整结束行
                        if is_two_cell_anchor:
                            original_to_row = image.anchor._to.row
                            if file_index > 0:
                                # 向上调整一行来修复图片下移问题
                                new_to_row = current_row + original_to_row - (start_row - 1) - 1
                            else:
                                # 同样向上调整一行
                                new_to_row = current_row + original_to_row - 1
                            image.anchor._to.row = new_to_row
                        
                        # 添加图片到合并表格
                        merged_ws.add_image(image)
                    except Exception as img_err:
                        print(f"警告：处理图片时出错：{str(img_err)}")
                        print(f"跳过此图片并继续处理...")
                        continue
    
                # 更新当前行位置
                current_row += ws.max_row - (start_row - 1)
            
            # 为第一行添加筛选功能
            merged_ws.auto_filter.ref = merged_ws.dimensions
            
            # -------------------- 新增代码：创建PDF制作状态工作表 --------------------
            # 创建"PDF制作状态"工作表
            pdf_status_ws = merged_wb.create_sheet(title="PDF制作状态")
            
            # 提取总表中的数据
            data = []
            headers = []
            for row in merged_ws.iter_rows(values_only=True):
                data.append(row)
            
            # 提取不重复的PDF文件名（从A列）
            pdf_files = set()
            for row in data[1:]:  # 跳过表头
                if row[0]:  # 确保A列有值
                    pdf_files.add(row[0])
            pdf_files = sorted(list(pdf_files))
            
            # 提取状态为"正常"的客户编号（从H列，J列为"正常"）
            customer_ids = set()
            for row in data[1:]:  # 跳过表头
                if row[8] == "正常" and row[7]:  # I列(索引8)为"正常"且H列(索引7)有值
                    customer_ids.add(str(row[7]))  # 转为字符串以处理非纯数字的客户编号
            customer_ids = sorted(list(customer_ids))
            
            # 设置PDF制作状态表头
            pdf_status_ws['A1'] = "PDF文件名"
            pdf_status_ws['B1'] = "制作状态"
            pdf_status_ws['C1'] = "PDF文件名二维码"  # 新增二维码列
            
            # 从D列开始设置客户编号列（比原来后移一列）
            for i, cust_id in enumerate(customer_ids):
                col = get_column_letter(i + 4)  # D列开始（原来是C列）
                pdf_status_ws[f'{col}1'] = cust_id  # 去掉"客户"二字，只显示编号
            
            # 创建临时目录存放二维码图片
            # temp_dir = os.path.join(folder, "temp_qrcodes")
            # os.makedirs(temp_dir, exist_ok=True)
            
            # 填充PDF文件名和初始状态
            for i, pdf_file in enumerate(pdf_files, start=2):  # 从第2行开始
                pdf_status_ws[f'A{i}'] = pdf_file
                pdf_status_ws[f'B{i}'] = "没制作"  # 初始状态
                
                # clean_pdf_name = pdf_file
                # if clean_pdf_name.lower().endswith(".pdf"):
                #     clean_pdf_name = clean_pdf_name[:-4]
                # # 为每个PDF文件生成二维码
                # qr_file = os.path.join(pdf_dir, clean_pdf_name+'_result_qr.svg')
                # # 提取pdf页码 P3-25-0324，其中3是页码
                # pagenum = clean_pdf_name.split("-")[0].replace("P", "").replace("p", "")
                # # 使用清理后的PDF文件名生成URL
                # url = 二维码地址 + "/baogong?pdffilename=" + clean_pdf_name
                # # 生成二维码
                # QR_Single_Code(link=url, pagenum=str(pagenum), output_file=qr_file)
                
                # 添加二维码图片到表格
                # img = XLImage(qr_file)
                # img.width = 100
                # img.height = 100
                # pdf_status_ws.add_image(img, f'C{i}')
                
                # 计算每个客户的记录数
                for j, cust_id in enumerate(customer_ids):
                    col = get_column_letter(j + 4)  # D列开始（原来是C列）
                    # 统计当前PDF文件中状态为"正常"且客户编号匹配的记录数
                    count = sum(1 for row in data[1:] if row[0] == pdf_file and row[8] == "正常" and str(row[7]) == cust_id)
                    pdf_status_ws[f'{col}{i}'] = count if count > 0 else 0  # 没找到记录时设为0，不留空
            
            # 设置列宽
            pdf_status_ws.column_dimensions['A'].width = 30  # PDF文件名列宽
            pdf_status_ws.column_dimensions['B'].width = 15  # 制作状态列宽
            pdf_status_ws.column_dimensions['C'].width = 15  # 二维码列宽
            for i in range(len(customer_ids)):
                col = get_column_letter(i + 4)  # D列开始（原来是C列）
                pdf_status_ws.column_dimensions[col].width = 12  # 客户编号列宽
            
            # 为显示二维码设置行高
            for i in range(2, len(pdf_files) + 2):
                pdf_status_ws.row_dimensions[i].height = 80  # 设置足够的行高显示二维码
            
            # 添加筛选
            pdf_status_ws.auto_filter.ref = f"A1:{get_column_letter(len(customer_ids) + 3)}{len(pdf_files) + 1}"
            
            # -------------------- 创建客户制作状态工作表 --------------------
            customer_status_ws = merged_wb.create_sheet(title="客户制作状态")
            
            # 添加表头
            customer_status_ws['A1'] = "客户编号"
            customer_status_ws['B1'] = "制作状态"
            customer_status_ws['C1'] = "PDF文件数量"
            customer_status_ws['D1'] = "总数"  # 改为"总数"
            customer_status_ws['E1'] = "没制作数量"  # 新增列
            customer_status_ws['F1'] = "制作中数量"  # 新增列
            customer_status_ws['G1'] = "制作好数量"  # 新增列 
            customer_status_ws['H1'] = "质检不合格数量"  # 新增列
            
            # 填充客户编号和初始状态
            for i, cust_id in enumerate(customer_ids, start=2):
                customer_status_ws[f'A{i}'] = cust_id
                customer_status_ws[f'B{i}'] = "没制作"  # 初始状态
                
                # 统计该客户涉及的PDF文件数量
                pdf_count = sum(1 for pdf in pdf_files if any(row[0] == pdf and str(row[7]) == cust_id and row[8] == "正常" for row in data[1:]))
                customer_status_ws[f'C{i}'] = pdf_count
                
                # 统计该客户的总记录数
                record_count = sum(1 for row in data[1:] if str(row[7]) == cust_id and row[8] == "正常")
                customer_status_ws[f'D{i}'] = record_count
                
                # 设置新增列的初始值
                customer_status_ws[f'E{i}'] = record_count  # 没制作数量初始等于总数
                customer_status_ws[f'F{i}'] = 0  # 制作中数量初始为0
                customer_status_ws[f'G{i}'] = 0  # 制作好数量初始为0
                customer_status_ws[f'H{i}'] = 0  # 质检不合格数量初始为0
            
            # 设置列宽
            customer_status_ws.column_dimensions['A'].width = 15  # 客户编号列宽
            customer_status_ws.column_dimensions['B'].width = 15  # 制作状态列宽
            customer_status_ws.column_dimensions['C'].width = 15  # PDF文件数量列宽
            customer_status_ws.column_dimensions['D'].width = 15  # 总数列宽
            customer_status_ws.column_dimensions['E'].width = 15  # 没制作数量列宽
            customer_status_ws.column_dimensions['F'].width = 15  # 制作中数量列宽
            customer_status_ws.column_dimensions['G'].width = 15  # 制作好数量列宽
            customer_status_ws.column_dimensions['H'].width = 15  # 质检不合格数量列宽
            
            # 添加筛选
            customer_status_ws.auto_filter.ref = f"A1:H{len(customer_ids) + 1}"
            
            # -------------------- 创建质检不合格工作表 --------------------
            qa_failed_ws = merged_wb.create_sheet(title="质检不合格")
            
            # 添加表头
            qa_failed_ws['A1'] = "今日质检不合格拍照记录"
            
            # 从B列开始设置客户编号列
            for i, cust_id in enumerate(customer_ids):
                col = get_column_letter(i + 2)  # B列开始
                qa_failed_ws[f'{col}1'] = cust_id  # 只显示编号
            
            # 初始化表格
            for i in range(2, 22):  # 预先添加20行空行
                # 设置第一列为空
                qa_failed_ws[f'A{i}'] = ""
                
                # 设置所有客户编号列为0
                for j, cust_id in enumerate(customer_ids):
                    col = get_column_letter(j + 2)  # B列开始
                    qa_failed_ws[f'{col}{i}'] = 0
            
            # 设置列宽
            qa_failed_ws.column_dimensions['A'].width = 30  # 第一列宽度
            for i in range(len(customer_ids)):
                col = get_column_letter(i + 2)
                qa_failed_ws.column_dimensions[col].width = 12  # 客户编号列宽
            
            # 添加筛选
            qa_failed_ws.auto_filter.ref = f"A1:{get_column_letter(len(customer_ids) + 1)}{21}"  # 20行数据
            
            # 记录处理结果
            results[folder] = len(files)
        except Exception as e:
            print(f"\n处理文件夹 {folder} 时出错: {str(e)}")
            print("可能是Excel文件已打开，请关闭所有Excel软件后再试。\n")
            results[folder] = -1  # 标记为出错
    
    print("所有文件夹处理完成！")
    return results

def QR_Single_Code(link="http://127.0.0.1:11435", pagenum="1", output_file="output_code_simple.svg", font_size=30):
    # 先导入必要的库
    import svgwrite
    from svgwrite import cm, mm
    
    # 创建QR码实例并获取二维码矩阵
    qr = qrcode.QRCode(
        version=None,
        error_correction=qrcode.constants.ERROR_CORRECT_H,
        box_size=10,
        border=4,
    )
    qr.add_data(link)
    qr.make(fit=True)
    qr_matrix = qr.get_matrix()
    
    # 创建SVG文档
    box_size = 10
    border = 4
    size = len(qr_matrix) * box_size + 2 * border * box_size
    dwg = svgwrite.Drawing(output_file, size=(size, size), profile='tiny')
    
    # 绘制二维码
    for y, row in enumerate(qr_matrix):
        for x, cell in enumerate(row):
            if cell:
                dwg.add(dwg.rect(
                    insert=((x + border) * box_size, (y + border) * box_size),
                    size=(box_size, box_size),
                    fill='black'
                ))
    
    # 计算中心区域
    center_size = min(size, size) // 6
    center_x = (size - center_size) // 2
    center_y = (size - center_size) // 2
    
    # 添加白色中心区域
    dwg.add(dwg.rect(
        insert=(center_x, center_y),
        size=(center_size, center_size),
        fill='white'
    ))
    
    # 添加页码文字
    text_size = font_size
    text_size = 80 # 设置页码字体大小
    dwg.add(dwg.text(
        pagenum,
        insert=(size // 2, size // 2 + text_size // 3),  # 调整以使文字居中
        font_size=text_size,
        font_family="Arial",
        text_anchor="middle",
        fill="black"
    ))
    
    # 保存SVG文件
    dwg.save()

def build_record_payload(record: dict) -> dict:
    payload = dict(record)
    path_value = record.get("black_box_path", "") or ""
    base_url = FILE_SERVER_BASE_URL or f"http://{get_local_ip()}:{FILE_SERVER_PORT}"
    if path_value:
        rel_path = path_value.replace(os.sep, "/").lstrip("/")
        payload["black_box_url"] = f"{base_url}/files/{quote(rel_path)}"
    else:
        payload["black_box_url"] = ""
    return payload

if __name__ == "__main__":
    print("🚀 角标识别程序启动...")
    init_databases()
    start_websocket_server()
    
    while True:
        run_script1()
        run_script2()
        # 无条件重新合并px表格，得到新的总表
        # merge_px_excel_files()
        print("当前PDF已处理完毕，请放入新的PDF文件，如果放入的PDF之前存在，请也删除对应PDF的XLSX文件！\n\n")
        time.sleep(1)
    
    input("执行完成，按任意键退出...")
